Documentation
¶
Overview ¶
Package auth is an embeddable authentication core for Go applications.
It owns its own tables in the application's database, queries them through sqlb and database/sql, and imports no ORM and no web framework, so any Go project can import it whatever its data layer and router are.
Shape ¶
The core knows users, sessions, verification tokens, the identity rules and the plugin runtime. It has no sign-in method and no identifier kind of its own: email, phone, username, password, OAuth and everything else are plugins, listed once in the call to New. That list is the complete answer to "what can sign a user in".
Every feature is one typed operation (Define). The Go API, the net/http handler in authhttp and the gin, echo and gqlgen adapters all call that one operation through one pipeline, so no feature is written twice and no entry point can skip a rate limit, a hook or the origin check.
Ownership ¶
Reads are open and writes are closed. An app may SELECT and JOIN the auth tables freely; it must change them only through this package, because the rules live here and not in the database.
Index ¶
- Constants
- Variables
- func BaseURLFor(ctx context.Context) string
- func CheckCookieName(name string) error
- func CheckEngine(engine string) error
- func CurrentScopes(ctx context.Context) (scopes []string, ok bool)
- func DecodeSessionData[T any](d AppData) (v T, ok bool, err error)
- func DeriveSecretKeys(label string, secrets ...[]byte) ([][]byte, error)
- func DetectDialect(ctx context.Context, db *sql.DB) (sqlb.Dialect, error)
- func DeviceKey(userAgent string) string
- func ErrorCodeConflicts() []string
- func ExpectedSchema(ms []Migration) map[string][]string
- func HasScope(ctx context.Context, scope string) bool
- func HashToken(t Token) []byte
- func IdentityBindings(dst *Identity) []sqlb.Binding
- func IsUniqueViolation(err error) bool
- func NewID(prefix string) (string, error)
- func Open(key []byte, sealed, aad string) ([]byte, error)
- func OriginOf(raw string) string
- func PickLocale[T any](byLocale map[string]T, locale string) (T, bool)
- func PluginOf[T Plugin](a *Auth) (T, error)
- func Quiet(ctx context.Context) context.Context
- func RateLimitedFor(retry time.Duration) error
- func RefuseNilEach[T any](o *OptionErrors, name string, vs []T) bool
- func RequestCookie(ctx context.Context, name string) (string, bool)
- func RequestHeader(ctx context.Context, name string) string
- func RequestHost(ctx context.Context) string
- func RequestTokenRejected(ctx context.Context) bool
- func RetryAfter(err error) (time.Duration, bool)
- func ScopeFrom(ctx context.Context) (string, bool)
- func Seal(key, plain []byte, aad string) (string, error)
- func SessionBindings(dst *Session) []sqlb.Binding
- func SessionData[T any](ctx context.Context) (v T, ok bool, err error)
- func SetSessionData(ctx context.Context, a *Auth, v any) error
- func SetUserColumn[T any](ctx context.Context, a *Auth, plugin, userID string, col sqlb.Expr[T], v T) error
- func TruncateRunes(s string, n int) string
- func UserBindings(dst *User) []sqlb.Binding
- func UserRateKey(_ context.Context, c *Call) (string, error)
- func VerificationBindings(dst *Verification) []sqlb.Binding
- func VerifiedIdentity() sqlb.Predicate
- func WithScope(ctx context.Context, tenantID string) context.Context
- type AccountDeletion
- type AppData
- type Auth
- func (a *Auth) AfterCommit(ctx context.Context, fn func(context.Context))
- func (a *Auth) AfterTx(ctx context.Context, fn func(context.Context))
- func (a *Auth) BaseURL(ctx context.Context) string
- func (a *Auth) CanPeekRates() bool
- func (a *Auth) CanReauthenticate(c *Call) error
- func (a *Auth) CheckCode(ctx context.Context, purpose Purpose, subject, code string, maxAttempts int) error
- func (a *Auth) CheckFields(f Fields) error
- func (a *Auth) CheckRemovable(ctx context.Context, userID, exceptIdentity, exceptCredential string) error
- func (a *Auth) CheckSchema(ctx context.Context) (SchemaReport, error)
- func (a *Auth) Close() error
- func (a *Auth) CompleteReauthentication(ctx context.Context, c *Call, ticket Token) (Reauthentication, error)
- func (a *Auth) CompleteSessionTask(ctx context.Context, c *Call, task SessionTaskID) (Session, error)
- func (a *Auth) Config() Config
- func (a *Auth) ConfirmDeleteAccount(ctx context.Context, t Token) error
- func (a *Auth) Conn(ctx context.Context) (sqlb.DBTX, error)
- func (a *Auth) ConsumeAllVerifications(ctx context.Context, purpose Purpose, identifier string) error
- func (a *Auth) ConsumeVerification(ctx context.Context, purpose Purpose, tok Token) (Verification, error)
- func (a *Auth) CreateIdentity(ctx context.Context, in NewIdentity) (Identity, error)
- func (a *Auth) CreateImpersonationSession(ctx context.Context, userID, adminID string, client Client, ttl time.Duration) (Session, Token, error)
- func (a *Auth) CreateMultiFactorSession(ctx context.Context, userID string, client Client) (Session, Token, error)
- func (a *Auth) CreateSession(ctx context.Context, userID string, client Client) (Session, Token, error)
- func (a *Auth) CreateSessionWithOptions(ctx context.Context, userID string, client Client, o SessionOptions) (Session, Token, error)
- func (a *Auth) CreateUser(ctx context.Context, in NewUser) (User, error)
- func (a *Auth) CredentialChanged(ctx context.Context, userID string) error
- func (a *Auth) DB() *sql.DB
- func (a *Auth) DeleteAccount(ctx context.Context) (DeleteAccountResult, error)
- func (a *Auth) DeleteIdentity(ctx context.Context, userID, id string) error
- func (a *Auth) DeleteSession(ctx context.Context, userID, id string) error
- func (a *Auth) DeleteUser(ctx context.Context, id string) error
- func (a *Auth) DeleteUserSessions(ctx context.Context, userID, exceptID string) error
- func (a *Auth) Deliver(ctx context.Context, to Identity, m Message) error
- func (a *Auth) DeliverRecovery(ctx context.Context, userID string, m Message) error
- func (a *Auth) DeliveryOutbox() bool
- func (a *Auth) DetectKind(raw string) (IdentifierKindName, string, error)
- func (a *Auth) DeviceSessions(ctx context.Context) (DeviceSessionsResult, error)
- func (a *Auth) Dialect() sqlb.Dialect
- func (a *Auth) DisabledOperations() []EndpointID
- func (a *Auth) Each(ctx context.Context, s Statement, fn func() error) error
- func (a *Auth) EmitSecurity(e SecurityEvent)
- func (a *Auth) EmitSecurityOf(c *Call, e SecurityEvent)
- func (a *Auth) EndSession(ctx context.Context, userID, id string, reason SessionEndReason) error
- func (a *Auth) EndUserSessions(ctx context.Context, userID, exceptID string, reason SessionEndReason) error
- func (a *Auth) Engine() string
- func (a *Auth) ErrorInfo(ctx context.Context, err error) ErrorInfo
- func (a *Auth) EvictCachedSessions(ctx context.Context, userID string) bool
- func (a *Auth) Exec(ctx context.Context, s Statement) (int64, error)
- func (a *Auth) FindIdentity(ctx context.Context, kind IdentifierKindName, raw string, verifiedOnly bool) (Identity, error)
- func (a *Auth) FindSignInIdentity(ctx context.Context, raw string) (Identity, IdentifierKind, error)
- func (a *Auth) FlushSecurity(ctx context.Context) error
- func (a *Auth) Identities(ctx context.Context, userID string) ([]Identity, error)
- func (a *Auth) IdentityByID(ctx context.Context, id string) (Identity, error)
- func (a *Auth) ImportUsers(ctx context.Context, users []ImportUser) ([]ImportResult, error)
- func (a *Auth) IssueCode(ctx context.Context, purpose Purpose, subject string, ttl time.Duration) (string, error)
- func (a *Auth) IssueCodeWith(ctx context.Context, purpose Purpose, subject string, ttl time.Duration, ...) (string, error)
- func (a *Auth) IssueVerification(ctx context.Context, purpose Purpose, identifier string, ttl time.Duration, ...) (Token, error)
- func (a *Auth) IssueVerificationToken(ctx context.Context, purpose Purpose, identifier string, tok Token, ...) error
- func (a *Auth) ListSessions(ctx context.Context) (SessionsResult, error)
- func (a *Auth) LoadUser(ctx context.Context, id string, dst Scannable) error
- func (a *Auth) LoadUserColumns(ctx context.Context, id string, bs ...sqlb.Binding) error
- func (a *Auth) Locale(ctx context.Context) string
- func (a *Auth) Logger() *slog.Logger
- func (a *Auth) Maintain(ctx context.Context, o MaintainOptions) (SweepResult, error)
- func (a *Auth) MergeFields(nested, top Fields) (Fields, error)
- func (a *Auth) Migrate(ctx context.Context) ([]string, error)
- func (a *Auth) MigrationSQL() (string, error)
- func (a *Auth) MigrationStatus(ctx context.Context) ([]MigrationState, error)
- func (a *Auth) Migrations() ([]Migration, error)
- func (a *Auth) NewID(prefix string) (string, error)
- func (a *Auth) Normalize(kind IdentifierKindName, raw string) (string, error)
- func (a *Auth) Now() time.Time
- func (a *Auth) One(ctx context.Context, s Statement) error
- func (a *Auth) Operation(id EndpointID) (Operation, bool)
- func (a *Auth) Operations() []Operation
- func (a *Auth) Orphans(ctx context.Context) ([]Orphan, error)
- func (a *Auth) PeekCode(ctx context.Context, purpose Purpose, subject, code string, maxAttempts int) error
- func (a *Auth) Plugins() []Plugin
- func (a *Auth) PromoteSession(ctx context.Context, c *Call) (Session, error)
- func (a *Auth) RateHit(ctx context.Context, key string, window time.Duration) (int, error)
- func (a *Auth) RatePeek(ctx context.Context, key string, window time.Duration) (int, error)
- func (a *Auth) Reauthenticate(ctx context.Context, c *Call, in ReauthAttempt) (Reauthentication, error)
- func (a *Auth) RefreshSession(ctx context.Context) (SessionResult, error)
- func (a *Auth) ReportReauthFailure(c *Call)
- func (a *Auth) RequireSchema(ctx context.Context) error
- func (a *Auth) Resolve(ctx context.Context) error
- func (a *Auth) ResolveSession(ctx context.Context, t Token) (sess *Session, user *User, refreshed bool, err error)
- func (a *Auth) RevokeDeviceSession(ctx context.Context, id string) error
- func (a *Auth) RevokeSession(ctx context.Context, id string) error
- func (a *Auth) RevokedSessions(ctx context.Context, userID string) ([]Session, error)
- func (a *Auth) RotateSessionToken(ctx context.Context, c *Call) (Token, error)
- func (a *Auth) RunInBackground(ctx context.Context, task func(context.Context))
- func (a *Auth) SQLTypes() SQLTypes
- func (a *Auth) SchemaMigration(engine, owner, name string, s TableSchema) (Migration, error)
- func (a *Auth) SecurityStats() []SinkStats
- func (a *Auth) Select() *sqlb.AnySelect
- func (a *Auth) SendOutbox(ctx context.Context) (OutboxResult, error)
- func (a *Auth) SendVerification(ctx context.Context, to Identity) error
- func (a *Auth) Session(ctx context.Context) (SessionResult, error)
- func (a *Auth) SessionStore() SessionStore
- func (a *Auth) Sessions(ctx context.Context, userID string) ([]Session, error)
- func (a *Auth) SetFields(ctx context.Context, userID string, f Fields) error
- func (a *Auth) SetPrimaryIdentity(ctx context.Context, userID, id string) (Identity, error)
- func (a *Auth) SignOut(ctx context.Context) error
- func (a *Auth) SignOutAll(ctx context.Context) error
- func (a *Auth) StartSpan(ctx context.Context, s SpanInfo) (context.Context, func(error))
- func (a *Auth) Sweep(ctx context.Context, retention time.Duration) (SweepResult, error)
- func (a *Auth) SwitchDeviceSession(ctx context.Context, id string) error
- func (a *Auth) TableName(t sqlb.Table) string
- func (a *Auth) TracksIP() bool
- func (a *Auth) Translate(ctx context.Context, code string) (string, bool)
- func (a *Auth) TrustsOrigin(origin string) bool
- func (a *Auth) TrustsReturnURL(target string) bool
- func (a *Auth) Tx(ctx context.Context, fn func(ctx context.Context, tx *sql.Tx) error) error
- func (a *Auth) UpdateProfile(ctx context.Context, in ProfileRequest) (UserView, error)
- func (a *Auth) UpdateUser(ctx context.Context, id string, in UserUpdate) (User, error)
- func (a *Auth) User(ctx context.Context, id string) (User, error)
- func (a *Auth) UserTables() []UserTable
- func (a *Auth) VerificationStore() VerificationStore
- func (a *Auth) VerifiedIdentities(ctx context.Context, userID string, kind IdentifierKindName) ([]Identity, error)
- func (a *Auth) VerifyIdentity(ctx context.Context, id string) (Identity, error)
- func (a *Auth) WaitDeliveries()
- func (a *Auth) WithRequest(ctx context.Context, r Request) context.Context
- type BackgroundFunc
- type BotChecker
- type BotCheckerFunc
- type BotSignals
- type CacheKey
- type Call
- func (c *Call) Annotate(key, value string)
- func (c *Call) Annotation(key string) string
- func (c *Call) Cookie(name string) (string, bool)
- func (c *Call) Header(name string) string
- func (c *Call) RedirectOnError(url string)
- func (c *Call) RequireOwner(ctx context.Context, o Ownership, id string) error
- func (c *Call) RequireOwnerRow(ctx context.Context, o Ownership, id string, cols ...sqlb.Binding) error
- func (c *Call) Respond(out any)
- func (c *Call) SetInput(v any)
- func (c *Call) SetSignedCookie(name, value string, expiresAt time.Time) error
- func (c *Call) SignInProven(ctx context.Context, kind IdentifierKindName, raw string, create bool) (ProvenSignIn, error)
- func (c *Call) SignInProvenAs(ctx context.Context, kind IdentifierKindName, raw string, newUser *NewUser) (ProvenSignIn, error)
- func (c *Call) SignedCookie(name string) (string, bool)
- type Client
- type Closer
- type CodeOptions
- type ColumnExtender
- type ColumnExtension
- type ColumnType
- type Config
- func (c Config) CanDeliver(name IdentifierKindName) bool
- func (c Config) DeriveKey(label string) ([]byte, error)
- func (c Config) DeriveKeys(label string) ([][]byte, error)
- func (c Config) HasIdentifier(name IdentifierKindName) bool
- func (c Config) HasSecret() bool
- func (c Config) Identifier(name IdentifierKindName) (IdentifierKind, bool)
- func (c Config) LoginKinds() []IdentifierKind
- func (c Config) PageSize(requested, fallback, limit int) int
- func (c Config) RecoveryKinds() []IdentifierKind
- func (c Config) Sealer() Sealer
- type ConfirmDeleteAccountRequest
- type Credential
- type CredentialCounter
- type CredentialListener
- type DataHooker
- type DataHooks
- type DeleteAccountResult
- type Deliverer
- type Dependent
- type DeviceRecognizer
- type DeviceRecognizerFunc
- type DeviceSessionRequest
- type DeviceSessionView
- type DeviceSessionsResult
- type DeviceSignIn
- type Done
- type Effect
- type EffectKind
- type Effects
- func (e *Effects) BindFlow(t Token, expiresAt time.Time)
- func (e *Effects) CacheSession(v string, expiresAt time.Time)
- func (e *Effects) ClearCache()
- func (e *Effects) ClearSession()
- func (e *Effects) ForgetDeviceSession(t Token)
- func (e *Effects) Header(name, value string)
- func (e *Effects) IssueSession(t Token, expiresAt time.Time)
- func (e *Effects) IssueSessionOf(s Session, t Token)
- func (e *Effects) List() []Effect
- func (e *Effects) Redirect(url string)
- func (e *Effects) RememberDevice(t Token, expiresAt time.Time)
- func (e *Effects) ReplaceSessionOf(s Session, t Token)
- func (e *Effects) SetCookie(name, value string, expiresAt time.Time) error
- type EndpointID
- type Error
- type ErrorInfo
- type ExclusiveVerificationStore
- type Fields
- type GeoLocation
- type GeoLocator
- type GeoLocatorFunc
- type GeoRules
- type GetAndDeleter
- type HookStage
- type IPReputation
- type IPReputationFunc
- type Identified
- type IdentifierKind
- type IdentifierKindName
- type IdentifierProvider
- type Identity
- type ImportIdentity
- type ImportResult
- type ImportUser
- type ImpossibleTravelRule
- type Initializer
- type Joiner
- type KeyRing
- type KindOption
- type MaintainOptions
- type Maintainer
- type MemorySecuritySink
- type Message
- type MessageCatalog
- type MessageDataDecoder
- type Messages
- type Migration
- type MigrationState
- type Migrator
- type MultiSessionConfig
- type NewIdentity
- type NewUser
- type Observer
- type ObserverFunc
- type Op
- func (o *Op[In, Out]) Call(ctx context.Context, in In) (Out, error)
- func (o *Op[In, Out]) Info() OpInfo
- func (o *Op[In, Out]) InputType() reflect.Type
- func (o *Op[In, Out]) Invoke(ctx context.Context, decode func(dst any) error) (any, error)
- func (o *Op[In, Out]) OutputType() reflect.Type
- func (o *Op[In, Out]) Plugin() string
- func (o *Op[In, Out]) Route() string
- type OpInfo
- type OpKind
- type Operation
- type OperationEvent
- type OperationHook
- type OperationHooker
- type OperationProvider
- type Option
- func EnvOptions(lookup func(string) (string, bool)) ([]Option, error)
- func Options(opts ...Option) Option
- func RenameTable(t sqlb.Table, realName string) Option
- func WithAccountDeletion(d AccountDeletion) Option
- func WithAllowedHosts(hosts ...string) Option
- func WithBackground(run BackgroundFunc) Option
- func WithBaseURL(u string) Option
- func WithBotCheck(c BotChecker, p RiskPolicy) Option
- func WithClock(now func() time.Time) Option
- func WithCodeLength(digits int) Option
- func WithDB(db *sql.DB, d sqlb.Dialect) Option
- func WithDBAuto(db *sql.DB) Option
- func WithDBRateStore() Option
- func WithDataHooks(h ...DataHooks) Option
- func WithDefaultLocale(tag string) Option
- func WithDefaultPageSize(n int) Option
- func WithDeferredSessionRefresh() Option
- func WithDeliveryOutbox(keys ...[]byte) Option
- func WithDeviceRecognizer(r DeviceRecognizer) Option
- func WithDeviceSessionExtras() Option
- func WithDialect(d sqlb.Dialect) Option
- func WithDisabledOperations(ids ...EndpointID) Option
- func WithErrorURL(url string) Option
- func WithFetchMetadata() Option
- func WithFreshWindow(d time.Duration) Option
- func WithGeoLocator(l GeoLocator) Option
- func WithGeoRules(r GeoRules) Option
- func WithIDs(gen func(prefix string) (string, error)) Option
- func WithIPReputation(c IPReputation, p RiskPolicy) Option
- func WithIPv6Prefix(bits int) Option
- func WithImpossibleTravel(r ImpossibleTravelRule) Option
- func WithLogger(l *slog.Logger) Option
- func WithLoopbackReturn() Option
- func WithMessages(c MessageCatalog) Option
- func WithMultiSession(max int) Option
- func WithObserver(o Observer) Option
- func WithOperationHooks(h ...OperationHook) Option
- func WithPendingSessionTTL(d time.Duration) Option
- func WithPlugins(ps ...Plugin) Option
- func WithProfileFields(cols ...string) Option
- func WithRateChallenger(ch RateChallenger) Option
- func WithRateExempt(fn RateExemptFunc) Option
- func WithRateLimit(limit int, window time.Duration) Option
- func WithRateRules(rules ...RateRule) Option
- func WithRateStore(r RateStore) Option
- func WithSQLTypes(engine string, t SQLTypes) Option
- func WithSealer(s Sealer) Option
- func WithSecondaryStorage(kv SecondaryStorage) Option
- func WithSecret(current []byte, previous ...[]byte) Option
- func WithSecurityLog(q SecurityQueue) Option
- func WithSecuritySink(sink SecuritySink, q SecurityQueue) Option
- func WithSession(c SessionConfig) Option
- func WithSessionArchive() Option
- func WithSessionCache(c SessionCache, ttl time.Duration) Option
- func WithSessionExtras(fn SessionExtras) Option
- func WithSessionPolicy(p SessionPolicy) Option
- func WithSessionRevocations(r SessionRevocations) Option
- func WithSessionStore(s SessionStore) Option
- func WithSignUpFields(cols ...string) Option
- func WithStatelessRefreshWithoutRead(interval time.Duration) Option
- func WithStatelessSessions(keys ...[]byte) Option
- func WithStatelessVersion(n int) Option
- func WithTablePrefix(prefix string) Option
- func WithTokenRotation(r TokenRotation) Option
- func WithTracer(t Tracer) Option
- func WithTravelHistory(h SignInHistory) Option
- func WithTrialAbuseCheck(c TrialAbuseChecker, p RiskPolicy) Option
- func WithTrustedOriginFunc(fn OriginFunc) Option
- func WithTrustedOrigins(origins ...string) Option
- func WithUnknownDeviceNotification() Option
- func WithUserColumns(cols ...ColumnExtension) Option
- func WithUserLocale(column string) Option
- func WithVerificationStore(s VerificationStore) Option
- func WithoutCSRFCheck() Option
- func WithoutIPTracking() Option
- func WithoutOriginCheck() Option
- func WithoutServerCallRateLimits() Option
- type OptionChecker
- type OptionErrors
- type OriginFunc
- type OriginTruster
- type Orphan
- type OrphanKind
- type OutboxResult
- type OwnerRefusalReporter
- type Ownership
- type Plugin
- type Principal
- type PriorSignIn
- type ProfileRequest
- type ProvenSignIn
- type Purpose
- type RateAction
- type RateChallenger
- type RateChallengerFunc
- type RateDecider
- type RateEvent
- type RateEventListener
- type RateExemptFunc
- type RateKey
- type RateKeyFunc
- type RateLimiter
- type RateObserver
- type RateOutcome
- type RatePeeker
- type RateRule
- type RateStore
- type ReauthAttempt
- type Reauthentication
- type Request
- type Requirement
- type RevokeSessionRequest
- type RiskErrorMode
- type RiskPolicy
- type RiskVerdict
- type SQLTypes
- type Scannable
- type SchemaColumn
- type SchemaIndex
- type SchemaReport
- type Sealer
- type SecondFactor
- type SecondFactorForSignIn
- type SecondaryStorage
- type SecurityEvent
- type SecurityKind
- type SecurityQueue
- type SecuritySeverity
- type SecuritySink
- type SecuritySinkFunc
- type Session
- type SessionCache
- type SessionCacheUserDropper
- type SessionChecker
- type SessionConfig
- type SessionEnd
- type SessionEndReason
- type SessionExtras
- type SessionLifecycle
- type SessionLifetime
- type SessionOptions
- type SessionPolicy
- type SessionPolicyInput
- type SessionReauthStore
- type SessionResult
- type SessionRevocations
- type SessionStore
- type SessionTask
- type SessionTaskError
- type SessionTaskID
- type SessionTasker
- type SessionTasks
- type SessionTokenRotator
- type SessionUserFinder
- type SessionView
- type SessionsResult
- type SetIfAbsenter
- type SignInAttempt
- type SignInHistory
- type SignInHistoryFunc
- type SignInMethod
- type SignUpIdentifier
- type SignUpInfo
- type SinkStats
- type Span
- type SpanInfo
- type SpanKind
- type Statement
- type SweepResult
- type Sweeper
- type TableHooks
- type TableOwner
- type TableSchema
- type Token
- type TokenDelivery
- type TokenPrefixLister
- type TokenResolver
- type TokenRotation
- type Tracer
- type Transport
- type TrialAbuseChecker
- type TrialAbuseCheckerFunc
- type TypedMigrator
- type UnknownDeviceData
- type User
- type UserTable
- type UserTableOwner
- type UserUpdate
- type UserView
- type Validator
- type Verification
- type VerificationSender
- type VerificationStore
- type WireToken
Constants ¶
const ( TableNameCode = "auth_code" // TableAliasCode is reserved by the core. TableAliasCode = "oc" )
One-time codes: short numeric codes a person types, sent by SMS or email.
They cannot be verification tokens. A token has 256 bits and is looked up by its hash; a six-digit code has a million values, so a lookup by hash would let anyone guess somebody's code. A code is therefore always checked against one named subject (an identity, a number, an address), with an attempt counter, and spent on success.
const ( // DefaultCodeLength is the number of digits of a code unless // WithCodeLength says otherwise. //ds:def id=defaultcodelength-3vn8hq2k owner=@khanakia stability=stable DefaultCodeLength = 6 // CodeDigits is the default length under its earlier name. CodeDigits = DefaultCodeLength // MinCodeLength and MaxCodeLength bound WithCodeLength: four digits is the // shortest a code can be and still survive DefaultCodeMaxAttempts guesses // with a chance below one in two thousand; ten digits is the longest // anyone types from a message, and the longest that fits an int64 with // room to spare. //ds:def id=codelengthbounds-7xq2mt5r owner=@khanakia stability=stable MinCodeLength = 4 MaxCodeLength = 10 // DefaultCodeMaxAttempts wrong guesses spend a code. A six-digit code // accepts one value in a million; five guesses give a guesser one chance // in 200 000 per code, and the rate limits bound the codes. //ds:def id=defaultcodemaxattempts-qcfgc2jc owner=@khanakia stability=stable DefaultCodeMaxAttempts = 5 )
Code defaults and bounds.
const ( TableNameUser = "auth_user" TableNameIdentity = "auth_identity" TableNameSession = "auth_session" TableNameVerification = "auth_verification" // TableNameMigrations is the migration ledger. It is written only by the // sqlb ledger. //ds:def id=tablenamemigrations-xra38puh owner=@khanakia stability=stable TableNameMigrations = "auth_migrations" )
Physical table names owned by the core.
Every table the library owns is prefixed "auth_". The prefix is load-bearing: a migration tool that diffs the whole database (Atlas) proposes to drop tables no schema declares, so a consumer excludes "auth_*" in one glob that keeps working as plugins add tables.
const ( TableAliasUser = "u" TableAliasIdentity = "i" TableAliasSession = "s" TableAliasVerification = "v" )
Table aliases reserved by the core. A plugin must not reuse one; New rejects a collision. Aliases live on the table, not the statement, so a plugin in another module can write an ON clause against a core column at package level.
const ( ColUserID = "id" ColUserName = "name" ColUserImage = "image" ColUserCreatedAt = "created_at" ColUserUpdatedAt = "updated_at" // ColUserSessionsValidAfter is the instant before which no session of // the user is accepted: DeleteUserSessions (sign out everywhere) sets it, // which is what ends stateless sessions, and cached ones once their // entry expires. NULL until the first such call. ColUserSessionsValidAfter = "sessions_valid_after" )
Column names on auth_user. The core owns exactly these; a plugin adds its own through ColumnExtender.
const ( ColIdentityID = "id" ColIdentityUserID = "user_id" ColIdentityKind = "kind" ColIdentityValue = "value" ColIdentityValueRaw = "value_raw" ColIdentityVerifiedAt = "verified_at" ColIdentityIsPrimary = "is_primary" ColIdentityScopeID = "scope_id" ColIdentityCreatedAt = "created_at" )
Column names on auth_identity.
const ( ColSessionID = "id" ColSessionUserID = "user_id" ColSessionTokenHash = "token_hash" ColSessionExpiresAt = "expires_at" ColSessionLastSeenAt = "last_seen_at" ColSessionIP = "ip" ColSessionUserAgent = "user_agent" ColSessionCreatedAt = "created_at" // ColSessionFactorPending marks a session waiting for a second factor. ColSessionFactorPending = "second_factor_pending" // ColSessionImpersonator is the admin behind an impersonation session. ColSessionImpersonator = "impersonated_by" // ColSessionTransient marks a session made without "remember me". ColSessionTransient = "transient" // ColSessionIdleSeconds, ColSessionRefreshSeconds and ColSessionMaxExpiresAt // are the lifetime chosen for the session when it was created (see // WithSessionPolicy), so a sliding refresh uses the session's own values. // 0 and NULL, as on rows from before the columns existed, mean the // SessionConfig defaults. ColSessionIdleSeconds = "idle_seconds" ColSessionRefreshSeconds = "refresh_seconds" ColSessionMaxExpiresAt = "max_expires_at" // ColSessionRevokedAt is when the session was ended before its expiry, // under WithSessionArchive, which keeps the row as an audit trail // instead of deleting it. NULL for a live session, and always NULL // without the option, whose sign-out deletes the row as before. ColSessionRevokedAt = "revoked_at" // ColSessionData is the app's JSON for the session (Session.Data), the // engine's document type; NULL for none, and always NULL on rows from // before the column existed. ColSessionData = "data" // ColSessionReauthenticatedAt is when the user last re-authenticated on // the session (Session.ReauthenticatedAt); NULL when never, and always // NULL on rows from before the column existed. ColSessionReauthenticatedAt = "reauthenticated_at" // ColSessionTasks is the session tasks still to complete // (Session.Tasks), their ids joined by commas; ” for none, and on rows // from before the column existed. ColSessionTasks = "pending_tasks" // ColSessionPreviousTokenHash is the hash of the token the session had // before its last rotation under WithTokenRotation, and // ColSessionPreviousValidUntil the end of that token's grace window; // both NULL until a rotation, and always NULL without the option. ColSessionPreviousTokenHash = "previous_token_hash" ColSessionPreviousValidUntil = "previous_valid_until" )
Column names on auth_session.
const ( ColVerificationID = "id" ColVerificationIdentifier = "identifier" ColVerificationPurpose = "purpose" ColVerificationTokenHash = "token_hash" ColVerificationExpiresAt = "expires_at" ColVerificationConsumedAt = "consumed_at" ColVerificationMeta = "meta" ColVerificationCreatedAt = "created_at" )
Column names on auth_verification.
const ( IDPrefixUser = "usr" IDPrefixIdentity = "idn" IDPrefixSession = "ses" IDPrefixVerification = "ver" )
Id prefixes. A prefixed id tells a reader of a log line or a support ticket what kind of row it names without a lookup.
const ( //ds:def id=core-envvars-h63nyypk owner=@khanakia stability=stable EnvBaseURL = "AUTH_BASE_URL" EnvTrustedOrigins = "AUTH_TRUSTED_ORIGINS" // comma-separated EnvFreshWindow = "AUTH_FRESH_WINDOW" EnvSessionIdle = "AUTH_SESSION_IDLE" EnvSessionMax = "AUTH_SESSION_MAX" EnvSessionRefresh = "AUTH_SESSION_REFRESH" // EnvSecret is the WithSecret value, used as its bytes: any string of at // least MinSecretSize characters, such as `openssl rand -base64 32`. EnvSecret = "AUTH_SECRET" )
Environment variables EnvOptions reads. Durations use Go syntax ("168h", "30m").
const ( // WarnCSRFCheckOff is logged by New under WithoutCSRFCheck. // //ds:def id=core-warncsrfcheckoff-8vq3nk2w owner=@khanakia stability=stable WarnCSRFCheckOff = "auth: WithoutCSRFCheck is on: cookie-authenticated mutations are accepted from any origin (CSRF)" // WarnOriginCheckOff is logged by New under WithoutOriginCheck. // //ds:def id=core-warnorigincheckoff-3kd8wq5n owner=@khanakia stability=stable WarnOriginCheckOff = "auth: WithoutOriginCheck is on: every return and redirect URL is accepted (open redirect, token-bearing links to any host)" // WarnServerCallLimitsOff is logged by New under // WithoutServerCallRateLimits. // //ds:def id=core-warnservercalllimitsoff-4nd8qw2k owner=@khanakia stability=stable WarnServerCallLimitsOff = "" /* 147-byte string literal not displayed */ )
Start-up warnings New logs while an escape hatch is on. Constants so a test and an operator's log search match the exact text.
const ( // DefaultTravelSpeedKmh is a little above an airliner's cruising speed, // so only a journey no flight could make is flagged. DefaultTravelSpeedKmh = 900.0 // DefaultTravelMinDistanceKm ignores hops shorter than typical GeoIP error. DefaultTravelMinDistanceKm = 100.0 )
Defaults of ImpossibleTravelRule.
const ( EnginePostgres = render.DialectPostgres EngineMySQL = render.DialectMySQL EngineSQLite = render.DialectSQLite )
Engine names, as sqlb reports them.
const ( WildcardAny = "*" WildcardOne = "?" )
Wildcard characters a pattern label may hold. A label with either is matched on its own (it never spans a dot): "*" is any run of characters and "?" exactly one, so "https://preview-*.example.com" trusts "preview-42.example.com" and "https://app-?.example.com" trusts "app-1.example.com". Only a first label that is "*" alone spans labels.
const ( // MaxDeliveryAttempts is how many sends one outbox row gets, the first // one after the commit included. A row that used them all stays in the // table, never sent again, until Sweep's retention removes it: a message // that failed five times over hours is not one the recipient still // waits for, and its link has usually expired. //ds:def id=core-maxdeliveryattempts-8nf3kw6t owner=@khanakia stability=stable MaxDeliveryAttempts = 5 // DeliveryRetryBase is the wait after the first failed send; each later // failure doubles it, up to DeliveryRetryMax. //ds:def id=core-deliveryretrybase-2vq7mh4x owner=@khanakia stability=stable DeliveryRetryBase = time.Minute // DeliveryRetryMax caps the wait between two sends of one row. DeliveryRetryMax = time.Hour // DefaultOutboxBatch is how many rows one SendOutbox call sends at most, // so a backlog is worked through by repeated calls rather than one call // that outlives its scheduler's timeout. //ds:def id=core-defaultoutboxbatch-5pd2zr8m owner=@khanakia stability=stable DefaultOutboxBatch = 100 )
Outbox retry bounds.
const ( // AttrRuleKey is the rate key kind ("ip", "identifier", "func"); with // AttrLimit, AttrWindow and AttrCount for SecurityRateLimit. AttrRuleKey = "rule_key" AttrLimit = "limit" AttrWindow = "window" AttrCount = "count" // AttrFailures is the failure count that crossed the limit, for // SecurityCredentialStuffing and SecurityLockout. AttrFailures = "failures" // AttrScope says which limit a stuffing event crossed: "password" (one // password on many accounts) or "global" (failures across everything). AttrScope = "scope" // AttrCountry and AttrPreviousCountry are ISO 3166-1 alpha-2 codes, for // SecurityGeoCountry (AttrCountry) and SecurityImpossibleTravel (both). AttrCountry = "country" AttrPreviousCountry = "previous_country" // AttrDistanceKm, AttrSpeedKmh and AttrElapsed describe an impossible // journey: the great-circle distance, the speed it implies and the time // since the previous sign-in. AttrDistanceKm = "distance_km" AttrSpeedKmh = "speed_kmh" AttrElapsed = "elapsed" // AttrReason is the short cause a risk check gave (RiskVerdict.Reason), // for SecuritySuspiciousIP, SecurityBot and SecurityTrialAbuse. Never an // identifier or an address. AttrReason = "reason" // AttrReauthStep is a ReauthStep* value, for SecurityReauthentication. AttrReauthStep = "reauth_step" // AttrProvider is the sign-in provider's name, for SecurityTokenReplay. AttrProvider = "provider" // AttrSessionID is the id of the session concerned, for // SecuritySessionTokenReuse. A session id is not a credential. AttrSessionID = "session_id" // AttrError is an error's text, for SecuritySessionEndHookFailed. It is // the hook's own message, which must not carry a secret. AttrError = "error" )
Attribute keys of SecurityEvent.Attrs. Which keys a kind sets is listed beside each; values are text so a sink can ship them as log fields.
const ( // ReauthStepFresh: the session is fresh again. ReauthStepFresh = "fresh" // ReauthStepSecondFactor: the first factor passed and a ticket was // issued for the second. ReauthStepSecondFactor = "second_factor" // ReauthStepFailed: a password, code or passkey was refused. ReauthStepFailed = "failed" )
Values of AttrReauthStep.
const ( // ScopePassword: one password failed on many accounts. ScopePassword = "password" // ScopeGlobal: failures across all accounts passed the limit. ScopeGlobal = "global" )
Values of AttrScope.
const ( // DefaultSecurityBuffer is how many events may wait for one sink. A full // queue drops the new event (see WithSecuritySink). DefaultSecurityBuffer = 1024 // DefaultSecurityBatch is the most events handed to a sink in one call. DefaultSecurityBatch = 100 // DefaultSecurityFlush is the longest an event waits for a batch to fill. DefaultSecurityFlush = 2 * time.Second // SecurityWriteTimeout bounds one WriteSecurity call. SecurityWriteTimeout = 10 * time.Second )
Defaults of a sink's queue.
const ( // KeyLabelStatelessSessions seals stateless session tokens when // WithStatelessSessions was given no key. KeyLabelStatelessSessions = "stateless-sessions" // KeyLabelSnapshotCache seals the client-held session snapshot when // NewSnapshotCache was given no key. KeyLabelSnapshotCache = "snapshot-cache" // KeyLabelCompactCache signs the client-held, readable session copy // when NewCompactSessionCache was given no key. KeyLabelCompactCache = "compact-cache" // KeyLabelTwoFactor is plugin/twofactor's label for the key that seals // enrolled TOTP secrets when twofactor.WithEncryptionKey was not given. KeyLabelTwoFactor = "twofactor" // KeyLabelOAuthTokens is plugin/oauth's label for the key that seals // stored provider tokens when oauth.StoreTokens was given no key. KeyLabelOAuthTokens = "oauth-tokens" // KeyLabelOAuthState is plugin/oauth's label for the key that seals the // flow state into the flow cookie under oauth.StateInCookie. KeyLabelOAuthState = "oauth-state" // KeyLabelAuthGate is authgate's label for the key that seals its // session and flow cookies, derived from authgate.WithSecret. KeyLabelAuthGate = "authgate" // KeyLabelDeliveryOutbox seals the delivery outbox's payloads when // WithDeliveryOutbox was given no key. KeyLabelDeliveryOutbox = "delivery-outbox" )
Labels under which the core derives its own keys from the secret. A plugin derives under its own label, conventionally its name, so no two features ever share a key even though they share the secret.
const ( // DefaultSessionCacheTTL is used when WithSessionCache gets 0. //ds:def id=defaultsessioncachettl-8knzqpgz owner=@khanakia stability=stable DefaultSessionCacheTTL = time.Minute // MaxSessionCacheTTL caps the TTL: it is how long a revoked or banned // session keeps working on a device that holds a cached entry, so it // stays short whatever the option says. //ds:def id=maxsessioncachettl-2sh2x4n6 owner=@khanakia stability=stable MaxSessionCacheTTL = 15 * time.Minute )
Session cache TTL bounds.
const ( // DefaultStatelessReadInterval is used when the option gets 0: five // minutes between full reads is one user query per active session per // five minutes, and a ban that waits five minutes at most. //ds:def id=defaultstatelessreadinterval-3kq8vz2m owner=@khanakia stability=stable DefaultStatelessReadInterval = 5 * time.Minute // MaxStatelessReadInterval caps the interval: it is how long a banned // user, or a session cut off by "sign out everywhere" or a password // reset, keeps working, so it is the same bound as MaxSessionCacheTTL // whatever the option says. //ds:def id=maxstatelessreadinterval-7wm2pq4x owner=@khanakia stability=stable MaxStatelessReadInterval = MaxSessionCacheTTL )
Bounds of WithStatelessRefreshWithoutRead's interval.
const ( MaxNameLength = 100 MaxImageLength = 2048 )
Profile limits. The name is shown back to other users, so it is bounded; the image is a URL the app will put in an <img>, so only https is taken.
const ( // DefaultTokenRotationGrace is how long a rotated-away token keeps // working when TokenRotation.Grace is 0: long enough for the requests a // page or an app had in flight when the new token was issued (other // tabs, parallel API calls) to land. // //ds:def id=defaulttokenrotationgrace-5qm8wx3k owner=@khanakia stability=stable DefaultTokenRotationGrace = time.Minute // MaxTokenRotationGrace is the longest grace New accepts. Every second // of grace is a second in which a stolen token works beside the real // one without being noticed, so a longer window is a misconfiguration. // //ds:def id=maxtokenrotationgrace-9vk2nq7t owner=@khanakia stability=stable MaxTokenRotationGrace = 10 * time.Minute )
Bounds of TokenRotation.Grace.
const ( // AnnotationOrganization is the id of the organization an operation // acted on, which the operation resolves itself (from the active // organization, a team's or an invitation's) and the audit log records. //ds:def id=core-annotationorganization-7w3kq9xm owner=@khanakia stability=stable AnnotationOrganization = "organization" )
Annotation keys the library's own plugins record.
const AppName = "app"
AppName is the owner name of what the app itself registers: its columns (WithUserColumns, written through SetUserColumn with this name) and its hooks (HookSourceApp). No plugin may take it, so an app column and a plugin column can never share an owner.
const CorePluginName = "core"
CorePluginName is the owner name of the core's own operations and migrations. No plugin may use it.
const DefaultDeleteAccountPage = "/account/delete"
DefaultDeleteAccountPage is the path of the app's page a confirmation link opens when AccountDeletion.Link is nil: BaseURL + this + "?token=…". The page posts the token to /account/delete/confirm.
const DefaultDeleteAccountTTL = time.Hour
DefaultDeleteAccountTTL is how long a confirmation link works. An hour covers a mailbox that is slow to receive and short enough that a link forgotten in an inbox cannot delete the account days later.
const DefaultFreshWindow = 10 * time.Minute
DefaultFreshWindow is how recent a session must be for a FreshSignIn operation. Ten minutes is long enough to finish a settings change and short enough that a laptop left open overnight cannot change the password.
const DefaultMaxDeviceSessions = 5
DefaultMaxDeviceSessions caps the accounts one device may hold at once when WithMultiSession is on. Five is what a person switching between a work and a personal account and a couple of shared ones needs; every extra token is one more cookie on every request.
const DefaultSweepRetention = 7 * 24 * time.Hour
DefaultSweepRetention is how long a spent or expired verification token stays after it stopped working. The rows are useful for a while — a support person can see that a reset link was used and when — and useless after.
const DefaultTransientIdleTimeout = 12 * time.Hour
DefaultTransientIdleTimeout: a session the user did not ask to remember ends after half a day without use, besides ending with the browser.
const DeliveryTimeout = 30 * time.Second
DeliveryTimeout bounds one message delivery. Delivery runs after the response, so without a bound a hung mail server would pile up goroutines.
const DetectTimeout = 5 * time.Second
DetectTimeout bounds the probe queries WithDBAuto runs inside New for a driver it does not know by type, so a database that does not answer fails New instead of hanging it.
const FetchSiteCrossSite = "cross-site"
FetchSiteCrossSite is the Sec-Fetch-Site value of a request from another site, the one WithFetchMetadata refuses for a mutation.
const HookSourceApp = "app"
HookSourceApp is SpanInfo.Source for a hook the app registered with WithOperationHooks; a plugin's hooks carry the plugin's name.
const IDPrefixDelivery = "dlv"
IDPrefixDelivery prefixes an outbox row's id.
const IPv6PrefixBits = 128
IPv6PrefixBits is the longest prefix WithIPv6Prefix accepts, the whole address. Shorter is coarser: 64 counts a typical home or mobile allocation as one client, which is what stops an attacker rotating through the 2^64 addresses such an allocation hands out.
const KeyLabelCodes = "codes"
KeyLabelCodes is the label of the keys a reusable code is derived or sealed with; see CodeOptions.Reuse.
const KeyLabelCookies = "cookies"
KeyLabelCookies is the WithSecret label of the key that signs plugin cookies (Call.SetSignedCookie).
const MaxGeneratedCodeLength = 64
MaxGeneratedCodeLength is the longest code CodeOptions.Generate may return, in bytes: a code is typed or pasted by a person, and a sealed copy must stay a short column value.
const MaxImpersonationTTL = time.Hour
MaxImpersonationTTL caps an impersonation session: long enough to reproduce a problem, short enough that a forgotten one ends by itself.
const MaxRiskReasonLen = 64
MaxRiskReasonLen bounds RiskVerdict.Reason on an event, so a vendor's long message cannot flood the stream.
const MaxSessionDataSize = 2048
MaxSessionDataSize bounds the encoded data of one session, in bytes. It is the largest size that still fits beside the session in a sealed cookie (a stateless token, a client snapshot) under the 4 KiB browsers allow per cookie: a device record or a handful of preferences fit, a document does not. One bound for every store, so switching stores never changes what an app may keep.
const MaxSessionLifetime = 400 * 24 * time.Hour
MaxSessionLifetime caps SessionLifetime.Max. A session that could live longer than about a year is a credential that never expires, whatever the policy meant; 400 days is the ceiling browsers apply to cookies too.
const MinPatternSuffixLabels = 2
MinPatternSuffixLabels is how many fixed labels a pattern must end with, to the right of its last wildcard: a pattern always names a domain the app owns, so "https://*.com", "https://*" and "https://api.*" are refused.
const MinSecretSize = 32
MinSecretSize is the shortest secret WithSecret accepts, in bytes: 32 bytes is 256 bits, the strength of the AES-256 keys derived from it, so a shorter secret would make every derived key weaker than its algorithm. `openssl rand -base64 32` produces 44 characters, which is plenty.
const MinVerificationCutoffTTL = 7 * 24 * time.Hour
MinVerificationCutoffTTL is the shortest life of a revocation cutoff (ConsumeAll, DeleteIdentifiers) in a key-value verification store: seven days, beyond the longest token lifetime the library issues by default (a 24-hour email verification). A cutoff must outlive every token issued before it, because once it expires those tokens would be accepted again; the store also stretches it to the longest lifetime it has itself issued.
const NoSessionRefresh = MaxSessionLifetime
NoSessionRefresh, as RefreshAfter, stops the sliding expiry: a session then ends exactly IdleTimeout after it was created, however active the user. It is MaxSessionLifetime because no session can be seen that long after its last use: its hard stop comes first.
const PendingSessionTTL = 10 * time.Minute
PendingSessionTTL is how long a user has to pass the second factor, unless WithPendingSessionTTL says otherwise.
const SealKeySize = 32
SealKeySize is the length of a key for Seal: AES-256.
const (
TableNameDeliveryOutbox = "auth_delivery_outbox"
)
The delivery outbox's table. A row is one message that was handed to Deliver and is not known to be sent yet: written in the transaction that produced the message, deleted once the deliverer accepted it.
Columns: id (IDPrefixDelivery), payload (the recipient identity and the message as JSON, sealed with the outbox key, because a verify link or a code in it is a credential), attempts (sends started so far), next_attempt_at (when the row may be sent again: the lease of the send in flight, or the backoff after a failure) and created_at (Sweep deletes the rows older than its retention).
const (
TableNameRateLimit = "auth_rate_limit"
)
The database rate store's table. Counters live in the app's own database, so every app instance shares them with no new infrastructure.
const WildcardHost = "*."
WildcardHost is the label that makes a trusted origin a pattern: "https://*.example.com" trusts every subdomain of example.com, at any depth, and not example.com itself. As the first label it spans one label or more; see WithTrustedOrigins for the rest of the grammar.
Variables ¶
var ( // ErrInternal stands for every error that is not safe to describe. ErrInternal = NewError("auth.internal", http.StatusInternalServerError, "Something went wrong. Try again.") // ErrSignedOut is returned when an operation needs a session and the call // has none. It says nothing about why: an expired, revoked and missing // session look the same to the caller. ErrSignedOut = NewError("auth.signed_out", http.StatusUnauthorized, "Sign in to continue.") // ErrImpersonationRefused is returned for a FreshSignIn operation on an // impersonation session. ErrImpersonationRefused = NewError("auth.impersonation_refused", http.StatusForbidden, "This cannot be done while acting as another user.") // ErrFreshSessionRequired is returned when an operation needs a fresh // session, such as changing a password, and the current one was neither // created nor re-authenticated (Reauthenticate) within the fresh window. ErrFreshSessionRequired = NewError("auth.fresh_session_required", http.StatusForbidden, "Sign in again to continue.") // ErrOriginNotTrusted is returned when a call that carries a session // cookie comes from an origin that is not trusted. It is the CSRF defence. ErrOriginNotTrusted = NewError("auth.origin_not_trusted", http.StatusForbidden, "This request came from a site that is not trusted.") // ErrBadRequest is returned when the input of an operation cannot be // decoded. It never echoes the input back. ErrBadRequest = NewError("auth.bad_request", http.StatusBadRequest, "The request is not valid.") // ErrUnknownOperation is returned by a transport that is asked for an // operation that no plugin registered. ErrUnknownOperation = NewError("auth.unknown_operation", http.StatusNotFound, "Not found.") )
Errors the core itself returns to callers. Plugins declare their own the same way, with their own prefix.
var ( ErrDuplicatePlugin = errors.New("auth: duplicate plugin name") ErrMissingDependency = errors.New("auth: plugin dependency not registered") ErrAliasConflict = errors.New("auth: table alias conflicts with a registered alias") ErrNonUniqueLogin = errors.New("auth: a sign-in or recovery identifier must be unique") ErrNoNormalizer = errors.New("auth: identifier kind has no normalizer") ErrBadIdentifier = errors.New("auth: identifier kind is not valid") ErrDuplicateKind = errors.New("auth: two plugins register one identifier kind") ErrNoDeliverer = errors.New("auth: the identifier kind's plugin cannot deliver messages") ErrNoSignInMethod = errors.New("auth: no sign-in method; add a plugin such as password or oauth") ErrColumnConflict = errors.New("auth: two owners for one column") ErrDuplicateOperation = errors.New("auth: two operations share an id or a route") ErrBadOperation = errors.New("auth: operation description is not valid") ErrUnknownHookTarget = errors.New("auth: hook names an operation that is not registered") ErrBadOrigin = errors.New("auth: origin is not an absolute URL") ErrBadSessionCheck = errors.New("auth: a session check can use only auth_session and auth_user") // ErrNilOption: an option that takes a func or an interface was given // nil, which would otherwise panic at the first request that calls it. // The error names the option; leave it out to keep its default. ErrNilOption = errors.New("auth: an option was given nil; leave the option out to keep its default") // ErrBadSessionConfig: a WithSession duration is negative. A zero field // takes DefaultSessionConfig's value instead. ErrBadSessionConfig = errors.New("auth: a session duration is negative") // ErrNoDatabase is returned when the database is needed and New was given // none. It is never mapped to "signed out": a missing database hidden // behind a sign-out looks like a working system that forgets everyone. ErrNoDatabase = errors.New("auth: no database configured; pass auth.WithDB") // ErrPluginNotRegistered is returned by PluginOf for a plugin that is not // in the list. ErrPluginNotRegistered = errors.New("auth: plugin not registered") // ErrNoVerificationSender is returned by SendVerification for a kind // whose plugin is no VerificationSender. ErrNoVerificationSender = errors.New("auth: the identifier's plugin cannot send a verification") )
Wiring errors, returned by New. Each is a deployment that would be broken in a way nobody notices until a user is stuck, so it is refused at start-up.
var ( // ErrUserNotFound: no user has this id. ErrUserNotFound = NewError("auth.user_not_found", http.StatusNotFound, "Not found.") // ErrIdentityNotFound covers a missing identity and an unverified one // asked for as verified. The two share one error so the difference cannot // be used to learn which addresses exist. ErrIdentityNotFound = NewError("auth.identity_not_found", http.StatusNotFound, "Not found.") // ErrIdentityTaken: the unique index refused the value, because another // account already has it verified (or at all, for kinds unique over every // row). ErrIdentityTaken = NewError("auth.identity_taken", http.StatusConflict, "This is already in use by another account.") // ErrIdentifierInvalid: the value is malformed for every kind that could // have taken it. It describes the input, not any account, so it is safe. ErrIdentifierInvalid = NewError("auth.identifier_invalid", http.StatusBadRequest, "This does not look right. Check it and try again.") // ErrTokenInvalid covers a wrong, used, expired and wrong-purpose token // alike. ErrTokenInvalid = NewError("auth.token_invalid", http.StatusBadRequest, "This link or code is not valid any more.") // ErrLastSignInMethod: removing this would leave the account with no way // in. ErrLastSignInMethod = NewError("auth.last_sign_in_method", http.StatusConflict, "Add another way to sign in before you remove this one.") // ErrNotVerified: the operation needs a verified identity. ErrNotVerified = NewError("auth.not_verified", http.StatusConflict, "Verify this first.") // ErrNoRecoveryChannel: the user has no verified identity that can // receive a recovery message. Callers that must not reveal this (forgot // password) swallow it. ErrNoRecoveryChannel = NewError("auth.no_recovery_channel", http.StatusConflict, "This account has no verified way to receive a message.") // ErrRateLimited: a rate rule refused the call. ErrRateLimited = NewError("auth.rate_limited", http.StatusTooManyRequests, "Too many attempts. Wait a moment and try again.") // ErrChallengeRequired: a RateChallenge rule is over its limit and the // call carried no proof. The client solves the challenge (a captcha: // plugin/captcha's token header) and sends the call again with it. 428 // Precondition Required, because waiting does not help and a proof does. ErrChallengeRequired = NewError("auth.challenge_required", http.StatusPreconditionRequired, "Complete the challenge and try again.") )
Errors the core returns to callers. Each is safe to show; see Error.
var ( // ErrGeoBlocked: a GeoRules violation under RateBlock. ErrGeoBlocked = NewError("auth.geo_blocked", http.StatusForbidden, "Sign-in is not available from your location.") // ErrImpossibleTravel: an impossible journey under RateBlock. ErrImpossibleTravel = NewError("auth.impossible_travel", http.StatusForbidden, "This sign-in looks unusual. Try again later or contact support.") )
var ( //ds:def id=core-errchecksum-brj9fryp owner=@khanakia stability=stable ErrChecksumMismatch = errors.New("auth: a migration changed after it was applied") ErrEngineNotSupported = errors.New("auth: engine not supported") )
Ledger errors.
var ( // ErrReauthUnsupported is returned when the session store cannot record a // re-authentication (a custom SessionStore without the // SessionReauthStore upgrade). Signing in again still works. ErrReauthUnsupported = NewError("auth.reauthentication_unsupported", http.StatusNotImplemented, "Confirming your identity is not available here. Sign in again to continue.") // ErrReauthTicketInvalid is returned by the second step for a ticket that // is wrong, spent, expired or issued to another session: the user starts // again from the first factor. ErrReauthTicketInvalid = NewError("auth.reauthentication_ticket_invalid", http.StatusBadRequest, "Confirm your password or passkey again.") )
Errors of re-authentication.
var ( // ErrSuspiciousIP: IPReputation flagged the address under RateBlock. ErrSuspiciousIP = NewError("auth.ip_blocked", http.StatusForbidden, "This request cannot be completed from your network.") // ErrBotDetected: BotChecker flagged the request under RateBlock. ErrBotDetected = NewError("auth.bot_blocked", http.StatusForbidden, "This request looks automated. Try again from a browser.") // ErrTrialAbuse: TrialAbuseChecker flagged the sign-up under RateBlock. ErrTrialAbuse = NewError("auth.signup_blocked", http.StatusForbidden, "We could not create this account. Contact support if you think this is a mistake.") // ErrRiskCheckFailed: a check returned an error under RiskFailClosed. ErrRiskCheckFailed = NewError("auth.risk_check_unavailable", http.StatusServiceUnavailable, "This request cannot be checked right now. Try again shortly.") )
Errors of the risk checks.
var ( TableUser = sqlb.Table{Name: TableNameUser, Alias: TableAliasUser} TableIdentity = sqlb.Table{Name: TableNameIdentity, Alias: TableAliasIdentity} TableSession = sqlb.Table{Name: TableNameSession, Alias: TableAliasSession} TableVerification = sqlb.Table{Name: TableNameVerification, Alias: TableAliasVerification} TableCode = sqlb.Table{Name: TableNameCode, Alias: TableAliasCode} TableRateLimit = sqlb.Table{Name: TableNameRateLimit} TableMigrations = sqlb.Table{Name: TableNameMigrations} )
Tables owned by the core. Package-level values, because a plugin in another module declares its joins against them before any statement exists.
TableCode, TableRateLimit and TableMigrations exist so an app can name those tables in RenameTable and Auth.TableName like any other; the core writes them through its own generators and nothing joins them.
var ( UserID = sqlb.Col[string](TableUser, ColUserID) UserName = sqlb.Col[string](TableUser, ColUserName) UserImage = sqlb.Col[string](TableUser, ColUserImage) UserCreatedAt = sqlb.Col[time.Time](TableUser, ColUserCreatedAt) UserUpdatedAt = sqlb.Col[time.Time](TableUser, ColUserUpdatedAt) // UserSessionsValidAfter is nil until the user first signed out // everywhere. UserSessionsValidAfter = sqlb.Col[*time.Time](TableUser, ColUserSessionsValidAfter) )
Typed columns on auth_user.
The Go type of each column is what makes every predicate and binding built on it checkable: comparing UserCreatedAt with a string does not compile. Nullable columns are pointer types, so null is visible in the type system.
var ( IdentityID = sqlb.Col[string](TableIdentity, ColIdentityID) IdentityUserID = sqlb.Col[string](TableIdentity, ColIdentityUserID) IdentityKind = sqlb.Col[IdentifierKindName](TableIdentity, ColIdentityKind) IdentityValue = sqlb.Col[string](TableIdentity, ColIdentityValue) IdentityValueRaw = sqlb.Col[string](TableIdentity, ColIdentityValueRaw) IdentityVerifiedAt = sqlb.Col[*time.Time](TableIdentity, ColIdentityVerifiedAt) IdentityIsPrimary = sqlb.Col[bool](TableIdentity, ColIdentityIsPrimary) IdentityScopeID = sqlb.Col[*string](TableIdentity, ColIdentityScopeID) IdentityCreatedAt = sqlb.Col[time.Time](TableIdentity, ColIdentityCreatedAt) )
Typed columns on auth_identity.
var ( SessionID = sqlb.Col[string](TableSession, ColSessionID) SessionUserID = sqlb.Col[string](TableSession, ColSessionUserID) SessionTokenHash = sqlb.Col[[]byte](TableSession, ColSessionTokenHash) SessionExpiresAt = sqlb.Col[time.Time](TableSession, ColSessionExpiresAt) SessionLastSeenAt = sqlb.Col[time.Time](TableSession, ColSessionLastSeenAt) SessionIP = sqlb.Col[string](TableSession, ColSessionIP) SessionUserAgent = sqlb.Col[string](TableSession, ColSessionUserAgent) SessionCreatedAt = sqlb.Col[time.Time](TableSession, ColSessionCreatedAt) SessionFactorPending = sqlb.Col[bool](TableSession, ColSessionFactorPending) SessionImpersonator = sqlb.Col[*string](TableSession, ColSessionImpersonator) SessionTransient = sqlb.Col[bool](TableSession, ColSessionTransient) // The session's own lifetime; see ColSessionIdleSeconds. SessionIdleSeconds = sqlb.Col[int64](TableSession, ColSessionIdleSeconds) SessionRefreshSeconds = sqlb.Col[int64](TableSession, ColSessionRefreshSeconds) SessionMaxExpiresAt = sqlb.Col[*time.Time](TableSession, ColSessionMaxExpiresAt) // SessionRevokedAt is nil for a live session; see ColSessionRevokedAt. SessionRevokedAt = sqlb.Col[*time.Time](TableSession, ColSessionRevokedAt) // SessionAppData is the app's JSON for the session; see Session.Data. SessionAppData = sqlb.Col[AppData](TableSession, ColSessionData) // SessionReauthenticatedAt is nil until a re-authentication; see // Session.ReauthenticatedAt. SessionReauthenticatedAt = sqlb.Col[*time.Time](TableSession, ColSessionReauthenticatedAt) // SessionPendingTasks is the pending session tasks; see Session.Tasks. SessionPendingTasks = sqlb.Col[SessionTasks](TableSession, ColSessionTasks) // SessionPreviousTokenHash and SessionPreviousValidUntil are the token // a WithTokenRotation rotation replaced and the end of its grace // window. They are never bound into a Session: the hash is a lookup // key only, like SessionTokenHash. SessionPreviousTokenHash = sqlb.Col[[]byte](TableSession, ColSessionPreviousTokenHash) SessionPreviousValidUntil = sqlb.Col[*time.Time](TableSession, ColSessionPreviousValidUntil) )
Typed columns on auth_session.
var ( VerificationID = sqlb.Col[string](TableVerification, ColVerificationID) VerificationIdentifier = sqlb.Col[string](TableVerification, ColVerificationIdentifier) VerificationPurpose = sqlb.Col[Purpose](TableVerification, ColVerificationPurpose) VerificationTokenHash = sqlb.Col[[]byte](TableVerification, ColVerificationTokenHash) VerificationExpiresAt = sqlb.Col[time.Time](TableVerification, ColVerificationExpiresAt) VerificationConsumedAt = sqlb.Col[*time.Time](TableVerification, ColVerificationConsumedAt) VerificationMeta = sqlb.Col[string](TableVerification, ColVerificationMeta) VerificationCreatedAt = sqlb.Col[time.Time](TableVerification, ColVerificationCreatedAt) )
Typed columns on auth_verification.
var ( // ErrSecretTooShort is returned by New when WithSecret got fewer than // MinSecretSize bytes, including an empty value from an unset variable: // a deployment that forgot its secret must not start with none. ErrSecretTooShort = errors.New("auth: the secret must be at least 32 bytes") // ErrNoSecret is returned by Config.DeriveKey when no WithSecret was // given, so a plugin can tell "derive one" from "the app passed none". ErrNoSecret = errors.New("auth: no secret configured; pass auth.WithSecret or an explicit key") )
Wiring errors of the secret.
var ( // ErrSessionDataTooLarge: the encoded value exceeds MaxSessionDataSize. // A programming error, not a client's, so a plain error. ErrSessionDataTooLarge = errors.New("auth: session data exceeds MaxSessionDataSize") // ErrSessionDataInvalid: the value does not encode as JSON (a channel, a // function, a cycle), or a hook set bytes that are not JSON. ErrSessionDataInvalid = errors.New("auth: session data is not JSON") // signed in with an API key: such a request has no session row to hold // data, and a key is not a device. ErrSessionDataUnavailable = NewError("auth.session_data_unavailable", http.StatusConflict, "This request has no session to hold data.") )
Errors of the session data API.
var ( // ErrUnknownTable is returned for a RenameTable whose table neither the // core nor a registered plugin declares: a hand-built sqlb.Table, or a // plugin's table whose plugin is not in WithPlugins. A rename nothing // uses would otherwise sit in the configuration looking applied. ErrUnknownTable = errors.New("auth: RenameTable names a table that no registered plugin or the core declares") // ErrTableNameConflict is returned when two tables would share one real // name, or one table is renamed twice. The symptom otherwise is one // CREATE TABLE succeeding and the next failing on a name that exists. ErrTableNameConflict = errors.New("auth: two tables would share one real name") // ErrBadTableName is returned for a real name or a prefix that is not a // plain SQL identifier, or that makes an identifier the engines cannot // hold (see maxIdentifierLength). ErrBadTableName = errors.New("auth: table name is not a valid SQL identifier") )
Table-name errors, returned by New.
var ColumnTypeValues = []ColumnType{ColumnText, ColumnTimestamp, ColumnBool, ColumnInt}
ColumnTypeValues is the canonical iteration order over column types.
var CookieNamePattern = regexp.MustCompile(`^[a-z][a-z0-9_]{0,31}$`)
CookieNamePattern is what a plugin cookie's name may look like: lower case, digits and underscores, starting with a letter, at most 32 characters, so the transport can put it in a cookie name as is.
var CoreUserColumns = []string{ColUserID, ColUserName, ColUserImage, ColUserCreatedAt, ColUserUpdatedAt, ColUserSessionsValidAfter}
CoreUserColumns is every column the core owns on auth_user, so New can refuse a plugin that declares one of them as its own.
var DefaultSessionConfig = SessionConfig{ IdleTimeout: 7 * 24 * time.Hour, MaxLifetime: 30 * 24 * time.Hour, RefreshAfter: 24 * time.Hour, }
DefaultSessionConfig: 7 days idle, 30 days at most, refreshed at most daily.
var EffectKindValues = []EffectKind{EffectIssueSession, EffectClearSession, EffectRedirect, EffectBindFlow, EffectCacheSession, EffectClearCache, EffectForgetDeviceSession, EffectRememberDevice, EffectHeader, EffectCookie}
EffectKindValues is the canonical iteration order over effect kinds, so a transport can check at test time that it handles every one of them.
var EnvVars = []string{EnvBaseURL, EnvTrustedOrigins, EnvFreshWindow, EnvSessionIdle, EnvSessionMax, EnvSessionRefresh, EnvSecret}
EnvVars is every variable EnvOptions reads, for a deployment's docs and a startup log.
var ErrAPIKeyRefused = NewError("auth.api_key_refused", http.StatusForbidden, "This cannot be done with an API key.")
ErrAPIKeyRefused is returned when an auth operation — sign-out, a password change, a session list — is called with an API key. Those operations are for people; a key acts for its user only in the app's own routes.
var ErrAccountDeletionOff = errors.New("auth: account deletion is off; pass auth.WithAccountDeletion")
ErrAccountDeletionOff is answered by DeleteAccount and ConfirmDeleteAccount when WithAccountDeletion was not given: the feature is off, which is the default, not a wiring mistake.
var ErrBadCookieName = errors.New("auth: bad cookie name")
ErrBadCookieName is the error for a plugin cookie name that does not match CookieNamePattern or is one of the core's own cookies.
var ErrBadHost = fmt.Errorf("auth: allowed host is not a host name")
ErrBadHost is returned by New for an allowed host that is not a host: it has a scheme or a path, or a wildcard in a place the pattern rule refuses.
var ErrBadSchema = errors.New("auth: table schema is not valid")
ErrBadSchema is a TableSchema SchemaMigration cannot write: a name that is not a lower-case identifier, a duplicate column or index, an unknown type, an index naming a column the table does not have, or no ID.
var ErrBadUserTable = errors.New("auth: user table is not valid")
ErrBadUserTable is returned by New for a UserTable that names a table its plugin does not declare, a column that is not an identifier, or a pair listed twice.
var ErrCodeInvalid = NewError("auth.code_invalid", http.StatusBadRequest, "This code is not right, or it has expired.")
ErrCodeInvalid covers a wrong, spent, expired and exhausted code alike.
var ErrDeleteAccountNeedsLink = errors.New("auth: account deletion by link needs auth.WithBaseURL or AccountDeletion.Link")
ErrDeleteAccountNeedsLink is returned by New when AccountDeletion asks for a confirmation link and neither a Link function nor a BaseURL exists to build one from.
var ErrDeviceSessionNotFound = NewError("auth.device_session_not_found", http.StatusNotFound, "That account is not signed in on this device.")
ErrDeviceSessionNotFound refuses a switch or a revoke naming a session the device does not hold. One answer whether the id is unknown, expired or another device's, so nothing is learnt from the difference.
var ErrFieldInvalid = NewError("auth.field_invalid", http.StatusBadRequest, "A field in this request cannot be set, or has the wrong type.")
ErrFieldInvalid refuses a sign-up field that is not one WithSignUpFields listed, or whose value does not have the column's type. It names neither, so a probe learns nothing about the schema.
var ErrImportFailed = errors.New("auth: some users were not imported")
ErrImportFailed is returned by ImportUsers when at least one row failed; the results say which.
var ErrMessageDataUndecodable = errors.New("auth: message data cannot be queued: no plugin decodes it for its purpose")
ErrMessageDataUndecodable is a Message.Data the delivery outbox cannot store: no registered MessageDataDecoder restores it for its Purpose, or it does not encode as JSON. Refusing at enqueue keeps the sending operation from succeeding while the template silently loses its data.
var ErrNoKeys = errors.New("auth: no sealing key given")
ErrNoKeys is returned by New when a ring-taking option got no key at all.
var ErrNoScope = NewError("auth.no_scope", http.StatusBadRequest, "The request does not name a workspace.")
ErrNoScope is returned when a kind that is unique per tenant is used without a tenant in ctx. Failing is the only safe answer: a lookup without a scope could match the same address in another tenant, which is a sign-in to the wrong account.
var ErrNotColumnOwner = errors.New("auth: plugin writes a column it does not own")
ErrNotColumnOwner is returned when a plugin writes a column it did not add.
var ErrOperationNotWired = errors.New("auth: operation called before auth.New registered its plugin")
ErrOperationNotWired is returned when an operation is called before auth.New registered it. It is a programming error: the plugin was not in the list.
var ErrProfileInvalid = NewError("auth.profile_invalid", http.StatusBadRequest, "Use a shorter name, and an image address that starts with https://.")
ErrProfileInvalid refuses a name or image outside the limits.
var ErrRatePeekUnsupported = errors.New("auth: the rate store cannot read a count without adding to it (not a RatePeeker)")
ErrRatePeekUnsupported is RatePeek on a rate store that is not a RatePeeker.
var ErrRateWindow = errors.New("auth: a rate window must be positive")
ErrRateWindow is RateHit or RatePeek with a window that is not positive.
var ErrRatesNotReady = errors.New("auth: rate counters are not ready during plugin Init; count from an operation")
ErrRatesNotReady is RateHit or RatePeek called before New built the rate limiter: from a plugin's Init. It is a programming error, never a client's answer; a plugin counts from its operations and asks CanPeekRates, which does answer during Init.
var ErrResourceForbidden = NewError("auth.resource_forbidden", http.StatusForbidden, "This belongs to another account.")
ErrResourceForbidden is RequireOwner's answer for another user's row when the Ownership asks to tell the two cases apart (Forbidden), as better-auth's requireResourceOwnership does by default.
var ErrResourceNotFound = NewError("auth.resource_not_found", http.StatusNotFound, "Not found.")
ErrResourceNotFound is RequireOwner's answer for a row that does not exist and, by default, for one that belongs to someone else: one answer for both, so an id cannot be probed for whether it exists.
var ErrSchemaBehind = errors.New("auth: schema is behind the code")
ErrSchemaBehind is what RequireSchema returns when the database lacks a migration, a table or a column the code needs; the wrapped text lists them.
var ErrSealedCorrupt = errors.New("auth: sealed value does not open; was the key changed?")
ErrSealedCorrupt is returned by Open for a value that does not open with the key and the additional data: tampered, copied to another row, or sealed with a different key.
var ErrSessionNotFound = errors.New("auth: session not found")
ErrSessionNotFound is what a SessionStore returns for a token hash it does not hold or a session that expired. The core turns it into ErrSignedOut; stores return this one so the core can tell "not there" from "the store failed", which must be a 500 and not a silent sign-out.
var ErrSessionTaskPending = NewError("auth.session_task_pending", http.StatusForbidden, "Finish setting up your account to continue.")
ErrSessionTaskPending is returned for an operation a session cannot call while it has a task to complete. The wire carries the code only; the client reads the tasks from the session result. A Go caller gets a *SessionTaskError, which errors.Is matches to this value.
ErrSessionsUnavailable is answered where a deployment keeps no session list: stateless sessions cannot be listed beyond the current one or revoked one by one, because nothing about them exists on the server.
var ErrTokenExists = errors.New("auth: verification token already stored")
ErrTokenExists is an Insert of a token hash the store already holds, from a store that refuses duplicates (ExclusiveVerificationStore). Random tokens never collide, so only a caller that inserts a chosen value meets it: a single-use claim such as plugin/oauth's ID-token replay guard, for which it is the answer "already claimed".
var ErrTooManyDeviceSessions = NewError("auth.too_many_device_sessions", http.StatusConflict, "Too many accounts are signed in on this device. Sign one out first.")
ErrTooManyDeviceSessions refuses a sign-in on a device that already holds MultiSessionConfig.Max accounts.
var ErrUnboundSessionStore = errors.New("auth: SQLSessionStore is bound by auth.New; use the store Auth.SessionStore returns")
ErrUnboundSessionStore is what SQLSessionStore's value answers when it is called directly instead of through the Auth that New bound it to.
var ErrUnknownDialect = errors.New("auth: cannot tell which SQL dialect the database speaks; pass it with auth.WithDB(db, dialect)")
ErrUnknownDialect is DetectDialect's answer for a database it cannot name as Postgres, MySQL or SQLite: pass the dialect with WithDB instead.
var ErrUserExists = errors.New("auth: a user with this id exists")
ErrUserExists refuses an import whose ID is already a user's.
var KindNamePattern = regexp.MustCompile(`^[a-z][a-z0-9_]{0,30}$`)
KindNamePattern is what a kind name may look like. It is strict because the name is embedded in generated DDL: a unique index WHERE clause and, on MySQL, a generated column name.
var OrphanKindValues = []OrphanKind{OrphanOwner, OrphanTable, OrphanColumn}
OrphanKindValues is the canonical iteration order over orphan kinds.
var RateActionValues = []RateAction{RateBlock, RateLog, RateChallenge}
RateActionValues lists every action, for a configuration screen or a test that walks them.
var RateOutcomeValues = []RateOutcome{RateOutcomeBlocked, RateOutcomeLogged, RateOutcomeChallenged, RateOutcomePassed}
RateOutcomeValues lists every outcome.
var ReauthStepValues = []string{ReauthStepFresh, ReauthStepSecondFactor, ReauthStepFailed}
ReauthStepValues lists every AttrReauthStep value, for a sink that routes by step and a test that walks them.
var RequirementValues = []Requirement{Public, SignedIn, FreshSignIn, PendingSecondFactor, SignedInOrPending}
RequirementValues is the canonical iteration order over requirements.
var ReservedAliases = []string{TableAliasUser, TableAliasIdentity, TableAliasSession, TableAliasVerification, TableAliasCode}
ReservedAliases is the canonical list of aliases the core occupies.
var RiskErrorModeValues = []RiskErrorMode{RiskFailOpen, RiskFailClosed}
RiskErrorModeValues lists every mode, for a configuration screen or a test.
var SecurityKindValues = []SecurityKind{ SecurityRateLimit, SecurityCredentialStuffing, SecurityLockout, SecurityGeoCountry, SecurityImpossibleTravel, SecurityStaleAccount, SecurityCompromisedPassword, SecuritySuspiciousIP, SecurityBot, SecurityTrialAbuse, SecurityReauthentication, SecurityTokenReplay, SecuritySessionTokenReuse, SecuritySessionEndHookFailed, }
SecurityKindValues lists every kind the library emits, for a sink that routes by kind and a test that walks them.
var SecuritySeverityValues = []SecuritySeverity{SeverityInfo, SeverityWarn, SeverityAlert}
SecuritySeverityValues lists every severity, lowest first.
var SessionEndReasonValues = []SessionEndReason{ SessionEndUnspecified, SessionEndSignOut, SessionEndRevoked, SessionEndRevokedOthers, SessionEndRevokedAll, SessionEndAdmin, SessionEndImpersonationStopped, SessionEndBanned, SessionEndPasswordChanged, SessionEndPasswordReset, SessionEndPasswordSet, SessionEndCredentialChanged, SessionEndTokenReuse, SessionEndProviderLogout, SessionEndUserDeleted, SessionEndExpired, }
SessionEndReasonValues lists every reason, for a consumer that validates or documents them.
var TableDeliveryOutbox = sqlb.Table{Name: TableNameDeliveryOutbox}
TableDeliveryOutbox declares the outbox table, so an app can rename it (RenameTable) and read it.
var TokenDeliveryValues = []TokenDelivery{DeliveryCookie, DeliveryBearer}
TokenDeliveryValues is the canonical iteration order over deliveries.
Functions ¶
func BaseURLFor ¶
BaseURLFor is Auth.BaseURL for a transport that holds only the request context (the request attached by WithRequest knows its Auth), or "" with no request in ctx. authhttp.CookieDomainFromBaseURL derives the cookie domain from it, so the domain is always an allowed host or the static base URL's, never a value the request chose.
func CheckCookieName ¶
CheckCookieName reports whether name may be a plugin cookie's name: CookieNamePattern, and none of the core's own cookies (session, flow, session_cache, device, account_*). A transport may call it to refuse a name before it reads one.
func CheckEngine ¶
CheckEngine returns ErrEngineNotSupported for an engine the core cannot write migrations for. Plugins call it first in Migrations.
func CurrentScopes ¶
CurrentScopes returns the scopes of the API key the request in ctx was made with. ok is false for a request with a session or no credential at all, so "no key" and "a key with no scopes" read differently.
func DecodeSessionData ¶
DecodeSessionData reads a session's data as T. ok is false when the session carries none; an error means the data is not a T, which happens when the app changed its type without a migration of the live sessions.
func DeriveSecretKeys ¶
DeriveSecretKeys is Config.DeriveKeys without an Auth: the key for label from every secret, current first, by the same HKDF-SHA256 under the same label namespace. It exists for a package that seals values with no auth.New behind it (authgate, a login gate with no database), so its keys come from one derivation rule rather than a copy that could drift. The errors are ErrNoSecret for no secret, ErrSecretTooShort for a secret under MinSecretSize (New refuses those before Config.DeriveKeys can see them, so the check changes nothing there), and ErrBadOperation for an empty label. label is a fixed constant, never built from input.
func DetectDialect ¶
DetectDialect names the dialect db speaks, so an app can pass a bare *sql.DB as it passes a pool to better-auth. It looks at the driver's type first, which costs nothing and covers pgx, lib/pq, go-sql-driver/mysql and the three common SQLite drivers. A driver it does not know by type, which is any wrapper such as otelsql, is asked with up to three probe queries: sqlite_version() answers only on SQLite, version() names PostgreSQL only on Postgres, and @@version answers only on MySQL and MariaDB. Anything else is ErrUnknownDialect, never a guess.
func DeviceKey ¶
DeviceKey reduces a user agent to what stays the same across updates of one browser on one machine: the string with its version numbers removed and its spaces folded, so "Chrome/129.0.0.0" and "Chrome/130.0.0.0" on the same system are one device. It is a heuristic, as anything read from a user agent is: two machines of the same make and browser share a key.
func ErrorCodeConflicts ¶
func ErrorCodeConflicts() []string
ErrorCodeConflicts returns the codes declared more than once with a different status or message, sorted: two plugins sharing a code would answer clients ambiguously. A documentation generator or a wiring test fails on a non-empty list; NewError itself does not panic, because a test may build an Error with a known code on purpose.
func ExpectedSchema ¶
ExpectedSchema returns the tables and columns, in definition order, that the given migrations create: what CheckSchema compares the database with. Exported for tools and tests that want the derived schema without a database.
func HasScope ¶
HasScope reports whether the request's API key carries scope. A request with a session instead of a key returns false: an app that serves both decides separately what a signed-in person may do.
func HashToken ¶
HashToken returns the SHA-256 of a raw token, which is the only form the database ever stores. A database backup therefore contains no live session.
A fast hash is correct here, unlike for passwords: the input has 256 bits of entropy, so there is nothing for a slow hash to protect against.
func IdentityBindings ¶
IdentityBindings binds every identity column to dst.
func IsUniqueViolation ¶
IsUniqueViolation reports whether err is a unique-constraint failure on any supported engine. Plugins use it for their own tables, so a duplicate becomes a declared error instead of a 500.
func NewID ¶
NewID is the default id generator: "<prefix>_<24 random base32 characters>". Ids are made in Go before an INSERT, so no statement needs RETURNING, which MySQL does not have. The error is always nil (see randomBytes); the signature is the one WithIDs takes, so a replacement generator can fail.
func Open ¶
Open reverses Seal: it returns the plaintext of a value sealed with key and aad, or ErrSealedCorrupt for anything else, without saying which check failed, so a probe learns nothing about the key.
func OriginOf ¶
OriginOf is originOf for transports, which need the same reduction to turn a Referer header into an origin.
func PickLocale ¶
PickLocale returns the entry of byLocale that best fits locale: the exact tag, then its base language ("de-CH" falls back to "de"), compared without regard to case. It is the one lookup rule every delivering plugin uses, so an app's translations are found the same way for mail and for SMS.
func PluginOf ¶
PluginOf returns the registered plugin of type T. Generic so a caller gets its concrete type without an unchecked assertion that could panic.
func Quiet ¶
Quiet returns ctx with delivery switched off: Deliver records nothing and sends nothing for the rest of the request or transaction. It is for an import or a repair that creates identities without a person on the other end of the mail; a verify link sent during a migration of ten thousand accounts is ten thousand confused users.
func RateLimitedFor ¶
RateLimitedFor returns the error a rate rule answers with, carrying retry for RetryAfter, so a plugin's own limit (an API key's quota) answers the same code and Retry-After as the core's rules instead of a second code.
func RefuseNilEach ¶
func RefuseNilEach[T any](o *OptionErrors, name string, vs []T) bool
RefuseNilEach is RefuseNil for a variadic option: it records ErrNilOption naming name when any element of vs is nil, and reports whether it did. A generic function rather than a method because Go methods cannot have type parameters.
func RequestCookie ¶
RequestCookie is Call.Cookie for code that holds only the context, such as a data hook.
func RequestHeader ¶
RequestHeader reads a header of the request in ctx, or "" outside a request or for a transport without headers. It lets a data hook, which has no Call, read what the client sent with the call that caused the write.
func RequestHost ¶
RequestHost returns the host of the request in ctx as the transport reported it, or "" outside a request. Information only: BaseURL is the checked form.
func RequestTokenRejected ¶
RequestTokenRejected reports whether the request in ctx presented a session token that names no session: unknown, expired, revoked, or created before the user's SessionsValidAfter. A transport tells a client that keeps its own token (bearer mode) to forget it, because no call will ever accept it again. It is false for a pending second-factor session, which is alive and still needed to finish the sign-in; for a request without a token; for a lookup that failed with an error other than "signed out"; and before anything resolved the session (an operation that needs one, Resolve, or CurrentUser after middleware). Like CurrentUser it never queries by itself.
func RetryAfter ¶
RetryAfter returns how long a rate-limited caller should wait: the rule's window, an upper bound, because a fixed window may reopen sooner.
func Seal ¶
Seal encrypts a secret a plugin must store and read back, such as a TOTP secret or a provider's refresh token, with AES-256-GCM. aad binds it to its row — pass the row's id — so a sealed value copied onto another row does not open. The result is base64 text, safe for any text column.
func SessionBindings ¶
SessionBindings binds a session to dst. The token hash is absent on purpose: a session is looked up by its hash and never selected with it, so the secret stays off every result set, logged ones included.
func SessionData ¶
SessionData reads the data of the request's session in ctx as T, with no query: the session was resolved once for the request and the data came with it, from the store, the token or the cache. ok is false for a signed-out request, a context that is not a request, and a session with no data.
func SetSessionData ¶
SetSessionData replaces the data of the request's session in ctx with v, encoded as JSON, and makes the rest of the request see it. The write goes to the store (the Session.BeforeUpdate and AfterUpdate hooks run around it, in the transaction), or in stateless mode into a new token the transport hands to the client; a cache entry for the session is dropped, so the next request reads the new value. Nil v clears the data.
It refuses a signed-out request with ErrSignedOut and an API-key request with ErrSessionDataUnavailable, and a value over MaxSessionDataSize with ErrSessionDataTooLarge, before anything is written.
func SetUserColumn ¶
func SetUserColumn[T any](ctx context.Context, a *Auth, plugin, userID string, col sqlb.Expr[T], v T) error
SetUserColumn writes a column that plugin added to auth_user, for userID.
It refuses a column the plugin did not add. That check is the enforcement point for "writes are closed": a plugin also holds the transaction and could run any SQL, so this is the one sanctioned path, and a source test keeps plugins in this module on it.
func TruncateRunes ¶
TruncateRunes cuts s to at most n characters, never inside a multi-byte character: a byte slice such as s[:n] can end halfway through "é" and store invalid UTF-8, which Postgres refuses outright. Plugins bound user-typed labels with it.
func UserBindings ¶
UserBindings binds every core user column to dst.
A function rather than a shared slice because each binding holds a pointer into dst: a package-level slice would be shared mutable state between concurrent queries.
func UserRateKey ¶
UserRateKey is the RateKeyFunc that counts a ByFunc rule per signed-in user: the per-account rule of an operation whose input names no account because the caller is the account (a re-authentication, a step-up). A signed-out call is not counted by it, so pair it with a ByIP rule.
func VerificationBindings ¶
func VerificationBindings(dst *Verification) []sqlb.Binding
VerificationBindings binds a verification row to dst, without its hash.
func VerifiedIdentity ¶
VerifiedIdentity restricts identity rows to those whose ownership is proven. It is the one definition of the rule, so a plugin that matches an identity to a person cannot write a weaker version of it by accident.
Types ¶
type AccountDeletion ¶
type AccountDeletion struct {
// ConfirmByLink makes /account/delete send a confirmation link rather
// than delete; /account/delete/confirm then deletes. The user needs a
// verified recovery identity, or the call answers ErrNoRecoveryChannel.
ConfirmByLink bool
// LinkTTL is how long the link works; zero means DefaultDeleteAccountTTL.
LinkTTL time.Duration
// Link builds the URL the message carries from the token. Nil means
// BaseURL + DefaultDeleteAccountPage + "?token=…"; New refuses
// ConfirmByLink with neither a Link nor a BaseURL, since no link could
// be built.
Link func(t Token) string
}
AccountDeletion configures the self-service deletion operations, which exist only when WithAccountDeletion is given: deleting an account is a decision an app makes on purpose, and a route that appeared in every existing deployment would not be one.
With the zero value, POST /account/delete deletes the account at once: the call already needs a FreshSignIn session, which is the same proof a password change needs. ConfirmByLink adds a second step for apps whose users mostly sign in through a provider and hold no password: the call sends a link to the user's recovery channel instead, and the account is deleted when the link is spent.
type AppData ¶
type AppData []byte
AppData is JSON the app attached to a session: the encoded value, or nil for none. It is a named type so the stores can save it as text on every engine (driver.Valuer, sql.Scanner) and so the Redis store and the sealed caches carry it as JSON inside the session's JSON (json.Marshaler, json.Unmarshaler) without re-encoding. Write it through EncodeSessionData or SetSessionData, which check the size and that it is JSON; read it through SessionData or DecodeSessionData.
func EncodeSessionData ¶
EncodeSessionData turns v into the data a session carries, refusing a value that does not encode or that exceeds MaxSessionDataSize; nil v is no data. It is the one way to produce an AppData from a Go value: a Session.BeforeCreate hook uses it to set the data a session starts with, SetSessionData uses it for the current session.
func (AppData) MarshalJSON ¶
MarshalJSON writes the data as it is, since it is JSON already, or null for none. It never fails: a value reaches an AppData only through normalized.
func (*AppData) Scan ¶
Scan reads NULL, text or bytes, which is what the three drivers return for a document column. The bytes are copied: a driver may reuse its buffer.
func (*AppData) UnmarshalJSON ¶
UnmarshalJSON keeps the raw JSON, mapping null back to none.
type Auth ¶
type Auth struct {
// contains filtered or unexported fields
}
Auth is the wired core. Construct it with New; the zero value is unusable.
func New ¶
New validates the configuration and wires the plugins.
Each step converts a production incident into a failed start:
- plugin names — unique, well formed, not "core"
- dependencies — a plugin that needs another must have it
- identifier kinds — coherent, unique per kind, deliverable when they recover
- table aliases — no ambiguous columns in an unrelated query
- column extensions — one owner per column; user tables — each one declared by its plugin, no pair twice
- table names — every rename aimed at a declared table, no two tables sharing one real name, every derived identifier within the engines' limit (tables.go)
- sign-in method — at least one, or nobody can get in
- plugin validation, then plugin init
- operations — ids and routes unique, hooks aimed at real operations
- trusted origins — each one an absolute URL
Before any of it, the session mode: one store or stateless, never both, and a stateless key or cache TTL that is what its documentation says.
func (*Auth) AfterCommit ¶
AfterCommit runs fn once the transaction in ctx commits, or at once when ctx has no transaction. A rolled-back transaction runs nothing.
func (*Auth) AfterTx ¶
AfterTx runs fn once the transaction in ctx ends, whether it commits or rolls back, or at once when ctx has no transaction. It is for a write that a failure must not undo, such as counting a wrong guess: the operation fails, its transaction rolls back, and the count still lands. fn must use the database handle, since the transaction is gone when it runs.
func (*Auth) BaseURL ¶
BaseURL returns the public URL to build links from for the request in ctx: the request's host under the base URL's scheme when WithAllowedHosts allows it, and the static Config.BaseURL otherwise, including outside a request. Plugins build every link they send from it, so a user on preview-42.example.com gets a link to preview-42.example.com. It never ends with a slash.
func (*Auth) CanPeekRates ¶
CanPeekRates reports whether RatePeek works on the configured store. It answers during a plugin's Init too, before the limiter is built, from the options: the database store and the per-process default always can, a WithRateStore store when it is a RatePeeker.
func (*Auth) CanReauthenticate ¶
CanReauthenticate reports whether the call may re-authenticate, with the refusal Reauthenticate would give: ErrSignedOut for no session or a pending one, ErrAPIKeyRefused, ErrImpersonationRefused, ErrReauthUnsupported. A re-authentication operation calls it first, before it reads c.User or checks a credential, so a refused caller costs no hash and an operation whose SignedIn requirement was removed by mistake refuses instead of panicking on a nil user.
func (*Auth) CheckCode ¶
func (a *Auth) CheckCode(ctx context.Context, purpose Purpose, subject, code string, maxAttempts int) error
CheckCode spends the open code of purpose for subject when code matches. maxAttempts wrong guesses spend it (0 means DefaultCodeMaxAttempts).
Every wrong guess is counted after the transaction in ctx ends (AfterTx): the failed operation rolls its transaction back, and a count that a failure rolls back is no count. The success path deletes the row, and a concurrent request that also matched finds nothing to delete, so a code works once.
func (*Auth) CheckFields ¶
CheckFields reports whether f may be written by SetFields: every name a WithSignUpFields column, every value of its type. Empty is valid. An operation calls it before it changes anything, so a bad field refuses the whole sign-up rather than half of it.
func (*Auth) CheckRemovable ¶
func (a *Auth) CheckRemovable(ctx context.Context, userID, exceptIdentity, exceptCredential string) error
CheckRemovable returns ErrLastSignInMethod when removing the identity exceptIdentity (or the plugin credential exceptCredential) would leave userID with no way in: no other verified sign-in identity and no plugin credential. Either argument may be empty.
func (*Auth) CheckSchema ¶
func (a *Auth) CheckSchema(ctx context.Context) (SchemaReport, error)
CheckSchema compares the database with what this configuration's migrations create: the ledger for pending and changed migrations, the catalogue for the tables and columns. It changes nothing. It reads the expected tables and columns from the migration statements themselves (CREATE TABLE and ALTER TABLE … ADD COLUMN), so a plugin's own table is checked with no declaration beyond its migration.
func (*Auth) Close ¶
Close waits for messages in flight, then calls Close on every plugin that implements Closer, in reverse wiring order, and returns the first error.
func (*Auth) CompleteReauthentication ¶
func (a *Auth) CompleteReauthentication(ctx context.Context, c *Call, ticket Token) (Reauthentication, error)
CompleteReauthentication is called by a second factor's re-authentication operation after it checked its code: it spends ticket, which must come from Reauthenticate on this same session, and refreshes the session. A wrong, spent or expired ticket, or one issued to another session, is ErrReauthTicketInvalid; the session requirements are Reauthenticate's.
func (*Auth) CompleteSessionTask ¶
func (a *Auth) CompleteSessionTask(ctx context.Context, c *Call, task SessionTaskID) (Session, error)
CompleteSessionTask marks task done on the call's own session, from an operation the task allows, after that operation proved the task is done (a second factor enrolled, an organization chosen). The session is replaced by one without the task, under a new token handed to the client (ReplaceSessionOf), as PromoteSession does: the old token stops working, so a token captured before the task was done cannot outlive it. When the last task is done the new session is an ordinary one. The app data moves to the new session, and SessionLifecycle plugins are told of a replacement. c.Session is updated in place, so the rest of the request sees the new session.
A session without the task pending is left as it is and returned: the same operation serves an enrolment that no task asked for. Refused: no session or a pending second factor (ErrSignedOut), an API key (ErrAPIKeyRefused), and an operation the task does not allow (ErrBadOperation), which is a wiring mistake, never a user's.
func (*Auth) Config ¶
Config returns a copy of the resolved configuration. The slices are cloned too: a struct copy alone would share their backing arrays, and a caller that edited cfg.Identifiers[0] would change the running policy.
func (*Auth) ConfirmDeleteAccount ¶
ConfirmDeleteAccount spends a confirmation link's token and deletes the account, through the same operation POST /account/delete/confirm runs.
func (*Auth) Conn ¶
Conn returns what a plugin must run its own statements on for ctx: the open transaction when there is one, the database otherwise. A plugin that used DB() directly inside a transaction would write outside it and escape the rollback.
func (*Auth) ConsumeAllVerifications ¶
func (a *Auth) ConsumeAllVerifications(ctx context.Context, purpose Purpose, identifier string) error
ConsumeAllVerifications spends every open token of purpose for identifier, such as every outstanding reset link once the password changed.
func (*Auth) ConsumeVerification ¶
func (a *Auth) ConsumeVerification(ctx context.Context, purpose Purpose, tok Token) (Verification, error)
ConsumeVerification spends a token of purpose and returns its row.
Single use is the store's guarantee (see VerificationStore): the SQL store matches only a row whose consumed_at is null, a key-value store reads and deletes the record in one atomic step, so of two requests with one token exactly one continues.
A wrong, used, expired or wrong-purpose token is ErrTokenInvalid, all alike.
func (*Auth) CreateIdentity ¶
CreateIdentity normalizes and stores an identity. A value the unique index refuses returns ErrIdentityTaken.
func (*Auth) CreateImpersonationSession ¶
func (a *Auth) CreateImpersonationSession(ctx context.Context, userID, adminID string, client Client, ttl time.Duration) (Session, Token, error)
CreateImpersonationSession starts a session as userID for the admin adminID, lasting ttl (at most MaxImpersonationTTL). The row records adminID, so the user's session list shows the impersonation. Only an admin plugin should call it, after its own authorization.
func (*Auth) CreateMultiFactorSession ¶
func (a *Auth) CreateMultiFactorSession(ctx context.Context, userID string, client Client) (Session, Token, error)
CreateMultiFactorSession starts a full session without asking SecondFactor plugins, for a sign-in method that is itself multi-factor: a passkey with user verification proves possession of the device and the user's PIN or biometric in one step. A method that proves one factor must call CreateSession instead, or a user who turned on two-factor loses it.
func (*Auth) CreateSession ¶
func (a *Auth) CreateSession(ctx context.Context, userID string, client Client) (Session, Token, error)
CreateSession starts a session for userID and returns it with its raw token. The token exists only in the return value and, through the IssueSession effect, in the client; the database stores its hash.
Sign-in plugins call this and never write auth_session themselves, so the session rules live in one place.
When a SecondFactor plugin requires a second factor for the user, the session is created pending: it lasts PendingSessionTTL, signs nobody in, and PromoteSession replaces it once the factor is passed. Sign-in plugins need no code for this; they report Session.SecondFactorPending to the client.
func (*Auth) CreateSessionWithOptions ¶
func (a *Auth) CreateSessionWithOptions(ctx context.Context, userID string, client Client, o SessionOptions) (Session, Token, error)
CreateSessionWithOptions is CreateSession with the sign-in's choices, such as "remember me". Hand the result to the client with Effects.IssueSessionOf, which carries the choice to the cookie.
func (*Auth) CreateUser ¶
CreateUser inserts a user and runs the User create hooks, in the transaction of ctx if there is one.
func (*Auth) CredentialChanged ¶
CredentialChanged tells every CredentialListener plugin, in plugin order, that userID's credential changed, inside the transaction of ctx when there is one. The plugin that changed the credential calls it after its own write and after ending the sessions it ends; the first error stops the call and rolls everything back with it.
func (*Auth) DeleteAccount ¶
func (a *Auth) DeleteAccount(ctx context.Context) (DeleteAccountResult, error)
DeleteAccount deletes the signed-in user's account, or sends the confirmation link when configured, through the same operation POST /account/delete runs. The request in ctx must hold a fresh session.
func (*Auth) DeleteIdentity ¶
DeleteIdentity removes one of a user's identities. It refuses to remove the last way into the account; see CheckRemovable.
func (*Auth) DeleteSession ¶
DeleteSession ends one session of userID. Deleting a session of another user matches nothing, which is how revoking by id stays safe. In stateless mode there is nothing to delete: the request's own session ends when the transport clears its token, and any other id is ErrSessionsUnavailable; under WithSessionRevocations the session's id also goes on the list, so the token stops opening at once, not only on this device. SessionLifecycle plugins are told first, in the same transaction, so a plugin that refuses leaves the session in place in every store; so are the DataHooks.SessionEnded hooks, with SessionEndUnspecified (use EndSession to say why). The user's SessionCache entries are evicted after the commit (EvictCachedSessions): the cache is keyed by token, which the server does not keep, so the user's other devices miss once too.
func (*Auth) DeleteUser ¶
DeleteUser removes a user and everything that belongs to it, in one transaction and in Go: there is no foreign key and no cascade (ADR-0002). The User BeforeDelete hooks run first, which is where an app removes its own rows that reference the user or refuses the deletion; then every registered user table (UserTables: the plugins' rows, the identities), the verification tokens and codes filed under the user or its identities, the sessions through the store and the SQL table, and last the user row. Stateless sessions end because the user row they load is gone. The user's SessionCache entries are evicted after the commit (EvictCachedSessions).
func (*Auth) DeleteUserSessions ¶
DeleteUserSessions ends every session of userID except exceptID, which may be empty to end them all. A password reset and an "undo email change" call it with no exception, because they are often the answer to a stolen account.
With no exception it also sets the user's SessionsValidAfter to now, in every mode: that is what ends stateless sessions, which have no row to delete, and it costs one update. With an exception it cannot, since the excepted session would end too, so stateless mode refuses that call with ErrSessionsUnavailable. Either way the user's SessionCache entries are evicted after the commit (EvictCachedSessions), so the end reaches a cached device on its next request.
func (*Auth) Deliver ¶
Deliver asks the plugin that owns the identity's kind to send m, after the transaction in ctx commits.
The send runs in the background, detached from the request. Two reasons: a request that waited for a mail server would take visibly longer when an account exists, which is exactly the timing signal "forgot password" must not give; and a slow provider must not slow sign-up. A failure is logged — the data it refers to is committed, and the user can ask again. WaitDeliveries waits for every send in flight, for tests and for a clean shutdown. Under Quiet(ctx) nothing is sent and nil is returned: an import creates identities with nobody waiting for a mail. Under WithDeliveryOutbox the message is also written to the outbox in ctx's transaction, so a send the process never made is retried by SendOutbox; a failed write fails Deliver.
func (*Auth) DeliverRecovery ¶
DeliverRecovery sends m to the user's best recovery channel: the first recovery kind, in plugin order, for which the user has a verified identity, preferring the primary one. ErrNoRecoveryChannel when there is none.
func (*Auth) DeliveryOutbox ¶
DeliveryOutbox reports whether WithDeliveryOutbox is on, so a scheduled job knows whether to call SendOutbox.
func (*Auth) DetectKind ¶
func (a *Auth) DetectKind(raw string) (IdentifierKindName, string, error)
DetectKind returns the first sign-in kind, in plugin order, whose normalizer and validator accept raw, with the normalized value. It is how one sign-in field takes an email address, a phone number or a handle. The built-in kinds do not overlap: a phone starts with "+", a handle has a letter and no "@".
func (*Auth) DeviceSessions ¶
func (a *Auth) DeviceSessions(ctx context.Context) (DeviceSessionsResult, error)
DeviceSessions lists the accounts signed in on the device of the request in ctx, through the same operation GET /device-sessions runs.
func (*Auth) DisabledOperations ¶
func (a *Auth) DisabledOperations() []EndpointID
DisabledOperations returns the ids WithDisabledOperations switched off, in no particular order, for operators and documentation tools.
func (*Auth) Each ¶
Each renders a read and runs it on the connection for ctx, calling fn after each row is scanned into the statement's bindings; an error from fn stops the iteration and is returned.
func (*Auth) EmitSecurity ¶
func (a *Auth) EmitSecurity(e SecurityEvent)
EmitSecurity offers an event to the security stream: for a plugin or an app with something worth reporting beside what the library emits. It never blocks and never fails; with no sink registered it does nothing. At, ID and Severity are filled when empty; IP is cleared under WithoutIPTracking so the promise on SecurityEvent holds for events plugins build.
func (*Auth) EmitSecurityOf ¶
func (a *Auth) EmitSecurityOf(c *Call, e SecurityEvent)
EmitSecurityOf is EmitSecurity for an event that happened inside the operation c: Op, Plugin, Transport and IP come from the call, and UserID from its signed-in user when the event names none. A plugin reporting a refused proof uses it, so its events carry what the core's own carry.
func (*Auth) EndSession ¶
EndSession is DeleteSession with the reason the session ends, which DataHooks.SessionEnded receives. Every core and plugin path that ends one session calls it, so a listener sees them all.
func (*Auth) EndUserSessions ¶
func (a *Auth) EndUserSessions(ctx context.Context, userID, exceptID string, reason SessionEndReason) error
EndUserSessions is DeleteUserSessions with the reason the sessions end, which DataHooks.SessionEnded receives once per session. Every core and plugin path that ends a user's sessions calls it.
func (*Auth) ErrorInfo ¶
ErrorInfo is ErrorOf in the request's language: the same code and status, with the message from the WithMessages catalogue when it has one for Locale(ctx) and the code, and then the declared message in OriginalMessage. A transport answers errors through it; a Go caller that shows errors to a person may too.
func (*Auth) EvictCachedSessions ¶
EvictCachedSessions drops every SessionCache entry of userID once the transaction in ctx commits, or at once outside one, so a ban or a suspension reaches the user's cached devices on their next request rather than when the entries expire. After the commit, because a drop before it would let a concurrent request cache the old state again.
It reports whether the eviction can happen: false with no cache or a cache that is not a SessionCacheUserDropper (the snapshot caches), where the user's devices keep their entries until the cache TTL. A failure of the cache is logged rather than returned: the write it follows is committed, and the entries still expire within the TTL. DeleteSession, DeleteUserSessions and DeleteUser call it; a plugin that suspends a user without ending sessions calls it itself.
func (*Auth) Exec ¶
Exec renders a write statement and runs it on the connection for ctx — the open transaction when there is one, the database otherwise (see Conn) — and returns the number of rows it matched.
It is how a plugin writes its OWN tables. Core tables stay closed to plugins whatever runs the statement: a plugin changes a core row only through the core API (CreateIdentity, SetUserColumn, …), and TestGuard_PluginsNeverWriteCoreTables enforces that on the syntax tree.
func (*Auth) FindIdentity ¶
func (a *Auth) FindIdentity(ctx context.Context, kind IdentifierKindName, raw string, verifiedOnly bool) (Identity, error)
FindIdentity returns the identity of kind with the value raw normalizes to, using the exact comparison the unique index serves.
A verified row always wins. Without verifiedOnly, and only when no verified row exists, the oldest unverified row is returned. Why "verified first" is two queries and not an ORDER BY verified_at DESC: Postgres sorts NULL first in a descending order and MySQL and SQLite sort it last, so one ORDER BY would pick a different row on each engine — and the wrong one on Postgres is the squatter's.
func (*Auth) FindSignInIdentity ¶
func (a *Auth) FindSignInIdentity(ctx context.Context, raw string) (Identity, IdentifierKind, error)
FindSignInIdentity resolves what a user typed into a sign-in field: it detects the kind among the sign-in kinds, then finds the identity as FindIdentity does. The kind is returned with it, so the caller can apply RequireVerified after checking the credential.
func (*Auth) FlushSecurity ¶
FlushSecurity returns once every event emitted before the call has been handed to its sink (and the sink has answered), or ctx ends. It is for tests and for a handler that must see its events delivered; the stream needs no flushing in normal operation. Nil when no sink is registered.
func (*Auth) Identities ¶
Identities returns every identity of a user, verified or not, for a settings page that shows pending verifications to their own owner.
func (*Auth) IdentityByID ¶
IdentityByID returns the identity with id, or ErrIdentityNotFound. It does not check the owner; a caller acting for a user compares UserID itself.
func (*Auth) ImportUsers ¶
func (a *Auth) ImportUsers(ctx context.Context, users []ImportUser) ([]ImportResult, error)
ImportUsers creates the users and their identities, each user in a transaction of its own, so a row that fails leaves nothing behind and the rest still land. Every plugin's data hooks run, so the email plugin's primary-address column is filled as it would be at sign-up; no message is sent, since an import is not a sign-up (Quiet). It returns one result per input, in order, and ErrImportFailed when any row failed. Passwords and other credentials are imported through their plugin's own import, after this, with the user ids the results carry.
func (*Auth) IssueCode ¶
func (a *Auth) IssueCode(ctx context.Context, purpose Purpose, subject string, ttl time.Duration) (string, error)
IssueCode replaces any open code of purpose for subject with a new one and returns it for delivery. subject names what the code is about and must be something only the sender can name: an identity id, or a normalized value.
func (*Auth) IssueCodeWith ¶
func (a *Auth) IssueCodeWith(ctx context.Context, purpose Purpose, subject string, ttl time.Duration, o CodeOptions) (string, error)
IssueCodeWith is IssueCode with options; see CodeOptions.
func (*Auth) IssueVerification ¶
func (a *Auth) IssueVerification(ctx context.Context, purpose Purpose, identifier string, ttl time.Duration, meta string) (Token, error)
IssueVerification stores a new single-use token for purpose and returns it. identifier names what the token is about; see Verification.Identifier.
func (*Auth) IssueVerificationToken ¶
func (a *Auth) IssueVerificationToken(ctx context.Context, purpose Purpose, identifier string, tok Token, ttl time.Duration, meta string) error
IssueVerificationToken stores tok, made by the caller, as a single-use token for purpose: better-auth's generateToken. The token is looked up by its hash alone, so it must be unguessable; the caller checks its strength (IssueVerification's are 256 random bits). An empty token is refused; the SQL store also refuses a token equal to one already stored.
func (*Auth) ListSessions ¶
func (a *Auth) ListSessions(ctx context.Context) (SessionsResult, error)
ListSessions returns the signed-in user's sessions, marking the current one, through the same operation the HTTP route runs.
func (*Auth) LoadUser ¶
LoadUser fills dst with the user id in one statement: every binding dst declares, and every join it asks for.
func (*Auth) LoadUserColumns ¶
LoadUserColumns reads chosen columns of one user, for a plugin that needs only its own columns.
func (*Auth) Locale ¶
Locale is the language of the request in ctx: the client's (Client.Locale) when it sent one, else WithDefaultLocale, else "". It is the one rule for messages sent and errors answered during the request, so a template and an error response never disagree about the user's language.
func (*Auth) Maintain ¶
func (a *Auth) Maintain(ctx context.Context, o MaintainOptions) (SweepResult, error)
Maintain runs every Maintainer plugin in registration order and returns their counters as "<plugin>.<counter>", in the same shape as SweepResult. A failing plugin stops the run after recording what it had done, so a later plugin never works on a broken earlier one's half-finished state; the error names the plugin. With no Maintainer registered it returns an empty result.
func (*Auth) MergeFields ¶
MergeFields joins the nested "fields" object and the top-level keys SplitBody returned. A top-level key that is not a WithSignUpFields column is ErrBadRequest, the answer an unknown key always got; a column sent in both places is ErrFieldInvalid, because neither value is obviously the one meant. The values themselves are checked later, by CheckFields.
func (*Auth) Migrate ¶
Migrate applies every migration not yet in the ledger and returns their "owner/name" labels in order.
One connection is pinned for the whole run, because the lock belongs to a connection. On Postgres and SQLite each migration and its ledger row commit together. MySQL commits every DDL statement on its own, so a failure part way through a migration there leaves its earlier statements applied and no ledger row; the error names the migration, and the operator repairs it by hand before running Migrate again.
func (*Auth) MigrationSQL ¶
MigrationSQL returns every migration's statements as one script, for review or for an app that applies SQL with its own tool.
func (*Auth) MigrationStatus ¶
func (a *Auth) MigrationStatus(ctx context.Context) ([]MigrationState, error)
MigrationStatus compares every migration with the ledger without changing anything. A missing ledger means nothing is applied yet.
func (*Auth) Migrations ¶
Migrations returns every migration in the order Migrate applies them: the core's, then for each plugin in list order its identity indexes, its column extensions and its own migrations.
It refuses an engine the core cannot write DDL for with ErrEngineNotSupported, once, here; every generator below may then assume a supported engine.
func (*Auth) Normalize ¶
func (a *Auth) Normalize(kind IdentifierKindName, raw string) (string, error)
Normalize reduces raw to the stored form for kind, and validates it.
func (*Auth) Now ¶
Now returns the current time from the configured clock, in UTC. Plugins use it for every expiry, so a test that freezes the clock freezes all of them.
UTC always, because SQLite stores a timestamp as text and compares it as text: two rows written in two time zones would compare by their wall-clock digits, and an expiry check would be off by the zone offset.
func (*Auth) One ¶
One renders a read and runs it on the connection for ctx, expecting exactly one row, which it scans into the statement's bindings. sqlb.ErrNoRows when there is none; the caller maps that to its own not-found error.
func (*Auth) Operation ¶
func (a *Auth) Operation(id EndpointID) (Operation, bool)
Operation returns the operation with id, or false.
func (*Auth) Operations ¶
Operations returns every registered operation in wiring order. Transports read this list and nothing else.
func (*Auth) Orphans ¶
Orphans lists what a removed plugin left behind. It only reports: the library never drops user data because configuration changed, so the operator decides what to delete.
A table counts as owned when the core has it or a registered plugin lists it through TableOwner. A plugin that owns a table must list it there, or its own table is reported here.
func (*Auth) PeekCode ¶
func (a *Auth) PeekCode(ctx context.Context, purpose Purpose, subject, code string, maxAttempts int) error
PeekCode is CheckCode without spending the code: a match leaves the row for a later CheckCode, so a form can tell the user their code is right before asking for the new password or address. A wrong guess still counts an attempt, so peeking cannot be used to guess faster than checking.
func (*Auth) PromoteSession ¶
PromoteSession replaces the call's pending session with a full one, after a second factor was passed. The token changes too, so a token captured before the second factor is worthless after it. The app data the pending session carried moves to the full one, and SessionLifecycle plugins are told of the replacement rather than of an end. Only a PendingSecondFactor operation can call it.
func (*Auth) RateHit ¶
RateHit counts one hit under key in the fixed window that contains now, on the configured rate store (WithRateStore, WithDBRateStore, WithSecondaryStorage, or the per-process default), and returns the new count. key is the plugin's own: prefix it with the plugin's name so two plugins never share a counter. A store failure is an error, never a count of zero, which would read as "under the limit".
func (*Auth) RatePeek ¶
RatePeek returns key's count in the current window without adding to it: for a guard that must refuse before it checks (so the answer cannot leak which guesses were right) but counts only failures. ErrRatePeekUnsupported on a store that is not a RatePeeker.
func (*Auth) Reauthenticate ¶
func (a *Auth) Reauthenticate(ctx context.Context, c *Call, in ReauthAttempt) (Reauthentication, error)
Reauthenticate is called by a sign-in method's re-authentication operation after it proved the call's user: it refreshes the call's session, or, when the user's second factor is due (asked exactly as at sign-in, with SignInAttempt.Reauthentication set), answers with a ticket for the second step. It never creates a session or issues a new token; in stateless mode the same session is resealed with the new time, as a sliding refresh does.
Refused: no session or a pending one (ErrSignedOut), an API key (ErrAPIKeyRefused), an impersonation (ErrImpersonationRefused), a store without SessionReauthStore (ErrReauthUnsupported).
func (*Auth) RefreshSession ¶
func (a *Auth) RefreshSession(ctx context.Context) (SessionResult, error)
RefreshSession slides the signed-in session's expiry when it is due, through the same operation POST /session/refresh runs; it exists only under WithDeferredSessionRefresh.
func (*Auth) ReportReauthFailure ¶
ReportReauthFailure tells the security stream that a re-authentication proof was refused (a wrong password, code or passkey). The operations call it beside returning their error; the audit log records the refusal by itself, as it does every mutation.
func (*Auth) RequireSchema ¶
RequireSchema is CheckSchema for start-up: nil when the database holds everything, ErrSchemaBehind (wrapping the report's wording) when not, and the database error otherwise. An app calls it after auth.New and before serving, so a deploy that forgot to migrate fails to start instead of failing its users one request at a time.
func (*Auth) Resolve ¶
Resolve forces the session lookup for the request in ctx and reports a failure other than "signed out". Middleware calls it so a broken database becomes a 500 at the edge rather than a silent sign-out.
func (*Auth) ResolveSession ¶
func (a *Auth) ResolveSession(ctx context.Context, t Token) (sess *Session, user *User, refreshed bool, err error)
ResolveSession turns a raw token into its session and user, and slides the expiry of an active session.
Expiry is checked by the store, so an expired session is not found at all rather than found and then compared in Go, where a caller could forget. Plugins' SessionChecker conditions apply on the same lookup, so a banned user fails on the next request, not when a cache expires; and a session created before the user's SessionsValidAfter is refused in every mode.
refreshed reports whether the expiry moved, so the transport can send the cookie again with the new expiry. In stateless mode a refresh makes a new token, which only the request pipeline delivers (as an issue-session effect); a direct caller keeps using the old token until it expires. ErrSignedOut covers an unknown, expired and revoked token alike, and a session still waiting for its second factor.
A session with tasks pending (Session.Tasks) is returned as it is: its user is proven, and a flow that hands the token on (a popup, a handoff) needs it. It is usable only for the operations its tasks allow, so a caller deciding access itself checks Tasks, as CurrentUser does.
func (*Auth) RevokeDeviceSession ¶
RevokeDeviceSession ends one of the device's other accounts.
func (*Auth) RevokeSession ¶
RevokeSession ends one of the signed-in user's sessions.
func (*Auth) RevokedSessions ¶
RevokedSessions lists the sessions of userID that were ended before their expiry and kept by WithSessionArchive, newest revocation first, ties by id; empty without the option, since nothing is kept then. It is the audit trail: when each device was signed out, and by which path a support person can tell from the time.
func (*Auth) RotateSessionToken ¶
RotateSessionToken gives the call's own session a new raw token: same row, id, creation time, expiry and lifetimes, so nothing about its freshness changes, and the token the client sent stops working at once, its cache entry dropped with it. An admin plugin uses it before impersonating, so the admin's own token can be kept in the flow cookie and reissued when the impersonation ends while a stolen impersonation cookie alone gains nothing. In stateless mode the session is sealed again. The caller hands the new token to the client itself (IssueSessionOf or BindFlow).
func (*Auth) RunInBackground ¶
RunInBackground hands task to the WithBackground runner now, for a plugin whose bookkeeping must not hold up the response (an API key's last-use stamp, better-auth's runInBackground): ctx loses its cancellation and keeps its values, the task is bounded by DeliveryTimeout like a delivery, and WaitDeliveries waits for it, so a test or a shutdown sees it finish. It does not wait for the call's transaction: a task that must see committed data goes through AfterCommit first. A failure is the task's to log.
func (*Auth) SQLTypes ¶
SQLTypes returns the column types of the app's engine: TypesFor with the WithSQLTypes overrides applied. A plugin reaches the same value through TypedMigrator.
func (*Auth) SchemaMigration ¶
func (a *Auth) SchemaMigration(engine, owner, name string, s TableSchema) (Migration, error)
SchemaMigration writes the migration that creates s for engine, owned by owner and named name, under the table's real name (RenameTable and WithTablePrefix apply). Idempotent like the core's own: CREATE TABLE IF NOT EXISTS, and indexes with IF NOT EXISTS, or inside the CREATE TABLE on MySQL, which has no CREATE INDEX IF NOT EXISTS. It creates no foreign key.
func (*Auth) SecurityStats ¶
SecurityStats reports every sink's counters, in registration order; nil when no sink is registered. Queued minus Delivered minus Failed is what is still waiting.
func (*Auth) SendOutbox ¶
func (a *Auth) SendOutbox(ctx context.Context) (OutboxResult, error)
SendOutbox sends the outbox rows that are due: never sent because their process died after the commit, or failed and past their backoff. It sends at most DefaultOutboxBatch, one at a time, each bounded by DeliveryTimeout. A row is claimed by one conditional update before its send, so several instances running SendOutbox at once never send one row twice; a row another call or the post-commit send holds is skipped. Without WithDeliveryOutbox it is ErrBadOperation. "auth sweep" calls it; an app with a scheduler of its own calls it every minute or so.
func (*Auth) SendVerification ¶
SendVerification asks the plugin that owns to.Kind for a fresh verification of to. A kind whose plugin cannot is ErrNoVerificationSender; a plugin that chose not to (an option left off) returns nil and sends nothing, so the caller cannot tell the two apart by timing, which is the point: a refused sign-in must take the same time whether or not a mail went out.
func (*Auth) Session ¶
func (a *Auth) Session(ctx context.Context) (SessionResult, error)
Session returns the signed-in user's session view, with the extras WithSessionExtras computes, through the same operation GET /session runs.
func (*Auth) SessionStore ¶
func (a *Auth) SessionStore() SessionStore
SessionStore returns the store sessions live in: the SQL store unless WithSessionStore replaced it, and nil in stateless mode. It exists so an app or a test can run sessiontest over the configured store, or wrap it.
func (*Auth) Sessions ¶
Sessions lists a user's live sessions, newest first; sessions created in the same instant come in descending id order, so the list is the same on every store and every call. In stateless mode only the request's own session is known: it is returned when ctx carries it for userID, otherwise ErrSessionsUnavailable.
func (*Auth) SetFields ¶
SetFields writes f to userID's auth_user row in one UPDATE, inside the transaction in ctx, after the same checks as CheckFields. Empty writes nothing. It is the sign-up path's one exception to "only the owner writes a column", and only for columns the app listed in WithSignUpFields.
func (*Auth) SetPrimaryIdentity ¶
SetPrimaryIdentity makes a verified identity the primary one of its kind.
func (*Auth) SignOut ¶
SignOut ends the session of the request in ctx, through the same operation the HTTP route runs.
func (*Auth) SignOutAll ¶
SignOutAll ends every session of the signed-in user.
func (*Auth) StartSpan ¶
StartSpan starts a span on the WithTracer tracer and returns the context to run the work in and the function that ends it; without a tracer it is a no-op that costs nothing. It is for a transport tracing what happens outside an operation, such as authhttp's plugin middleware; the pipeline starts its own operation and hook spans.
func (*Auth) Sweep ¶
Sweep deletes rows that can no longer do anything: expired sessions, verification tokens expired or spent more than retention ago, finished rate-limit windows (whenever the ledger shows that table was created, with or without WithDBRateStore on this Auth), delivery outbox rows created more than retention ago (the same way for WithDeliveryOutbox; Sweep never sends, SendOutbox does), and whatever each Sweeper plugin reports. It is idempotent and safe to run from several instances at once. Nothing calls it by itself — the library has no background workers — so an app runs it from a scheduled job, or with "auth sweep" from the CLI.
func (*Auth) SwitchDeviceSession ¶
SwitchDeviceSession makes session id the device's active account.
func (*Auth) TableName ¶
TableName returns the real name of a declared table under this Auth's configuration: the label itself by default, the prefixed or renamed one under WithTablePrefix or RenameTable. It is for the SQL the app and a plugin write by hand, DDL above all; a statement built with sqlb through a.Select, a.Exec, a.One or a.Each already renders the real name.
A table nothing declared is returned unchanged, because the app's own tables pass through here too when it builds a join by hand.
func (*Auth) TracksIP ¶
TracksIP reports whether sessions record the client's address: false under WithoutIPTracking. A plugin that records addresses of its own (an audit log) follows it, so one option covers every place an address is kept.
func (*Auth) Translate ¶
Translate returns the catalogue's text for code in the request's locale (Locale) and true, or "" and false when there is no catalogue or it lacks the code for that locale. It is ErrorInfo's lookup for text that is not an error: the fixed strings of a page the library renders itself, such as authhttp's error page, each keyed by a code that page documents, so one WithMessages catalogue translates the page and its errors together. A caller that gets false shows its English text and labels the page "en".
func (*Auth) TrustsOrigin ¶
TrustsOrigin reports whether origin ("scheme://host") may send cookie-authenticated writes: the BaseURL's origin, a trusted origin, one an OriginTruster plugin added, a subdomain of a wildcard pattern ("https://*.example.com"), or one the WithTrustedOriginFunc function accepts. A server-rendered form uses it to refuse a cross-site POST, including a sign-in, where no session cookie exists yet for the pipeline's own check to look at. An empty origin is never trusted.
func (*Auth) TrustsReturnURL ¶
TrustsReturnURL reports whether target may be the place a flow sends the browser to afterwards: a same-site path ("/account", not "//evil" and no backslash), an absolute URL whose origin TrustsOrigin accepts, or a URL in a custom scheme WithTrustedOrigins listed as "myapp://" (a deep link back into a native app), or, under WithLoopbackReturn, an http URL on the loopback address on any port. Everything else, "" included, is refused. It is the one rule every returnTo and redirectTo goes through. A custom-scheme return must carry nothing secret: any app on the device can register the scheme, so the session travels as a bearer token in the response, never in the URL. Under WithoutOriginCheck every non-empty target is accepted unless it holds a line break or a backslash.
func (*Auth) Tx ¶
Tx runs fn in one transaction and commits it when fn returns nil.
Why the library owns begin and commit rather than accepting an app's *sql.Tx: messages such as a verify email must go out only after the data they refer to is committed. If the app committed, the library could not know when to send, and an app that forgot one extra call would silently never send a verify link. Here the careless path is the correct path: the app writes its own rows with tx, every library call made with the ctx that fn receives joins the same transaction, and the messages go out after the commit.
A Tx inside a Tx joins the outer one; only the outermost commits.
func (*Auth) UpdateProfile ¶
UpdateProfile changes the signed-in user's name or image, through the same operation the HTTP route runs.
func (*Auth) UpdateUser ¶
UpdateUser changes a user's display fields and runs the User update hooks, in the transaction of ctx if there is one.
func (*Auth) UserTables ¶
UserTables lists every table DeleteUser clears for a user, in deletion order: the plugins' in plugin order, then the core's. Tools and tests read it to check that no table with a user column is left out.
func (*Auth) VerificationStore ¶
func (a *Auth) VerificationStore() VerificationStore
VerificationStore returns the store verification tokens live in, so a conformance test can run against the SQL store this Auth uses.
func (*Auth) VerifiedIdentities ¶
func (a *Auth) VerifiedIdentities(ctx context.Context, userID string, kind IdentifierKindName) ([]Identity, error)
VerifiedIdentities returns a user's verified identities of kind. It is the one way a plugin should match an identity to a person.
func (*Auth) VerifyIdentity ¶
VerifyIdentity marks an identity verified. A value that another account has already verified returns ErrIdentityTaken and changes nothing.
func (*Auth) WaitDeliveries ¶
func (a *Auth) WaitDeliveries()
WaitDeliveries blocks until every message handed to Deliver has been sent or has failed.
type BackgroundFunc ¶
BackgroundFunc runs task after the response, detached from the request. ctx is the request's context without its cancellation, so the task is not cut short by the client hanging up, but with its values, so a tenant scope or a trace survives into it. The function must return at once: the request is waiting on it.
type BotChecker ¶
type BotChecker interface {
CheckBot(ctx context.Context, s BotSignals) (RiskVerdict, error)
}
BotChecker judges whether a request comes from a script. It runs on the request's goroutine before the credentials are read.
type BotCheckerFunc ¶
type BotCheckerFunc func(ctx context.Context, s BotSignals) (RiskVerdict, error)
BotCheckerFunc adapts a function to BotChecker.
func (BotCheckerFunc) CheckBot ¶
func (f BotCheckerFunc) CheckBot(ctx context.Context, s BotSignals) (RiskVerdict, error)
CheckBot calls f.
type BotSignals ¶
type BotSignals struct {
// Op is the operation being called.
Op OpInfo
// Transport is how it arrived.
Transport Transport
// Client is the caller's address, user agent, origin and fetch site; IP
// is "" when the transport did not supply one.
Client Client
// contains filtered or unexported fields
}
BotSignals is what a BotChecker sees of one request. It carries no credential and no input: those are not the bot check's business and must not reach a third-party service.
func (BotSignals) Header ¶
func (s BotSignals) Header(name string) string
Header reads a request header the transport passed, or "" when there is none.
type CacheKey ¶
type CacheKey struct {
// TokenHash is the hash of the request's session token.
TokenHash []byte
// Client is what the client sent back from an earlier Put, or "".
Client string
}
CacheKey identifies one client's session in a SessionCache.
type Call ¶
type Call struct {
Auth *Auth
Op OpInfo
Plugin string
Transport Transport
Client Client
Credential Credential
// Session and User are nil for a signed-out call.
Session *Session
User *User
Effects *Effects
// Flow is the client's flow binding; see Request.Flow.
Flow Token
// Input is the operation's decoded input, for hooks: a Before hook that
// restricts sign-up to one mail domain asserts it to the operation's
// input type. It is a copy; assigning to it changes nothing the
// operation sees. A Before hook that means to change the input calls
// SetInput, which is checked against the operation's input type. It is
// nil until the input is decoded, which is after the rate, origin and
// session checks.
Input any
// contains filtered or unexported fields
}
Call is what an operation sees of the call that invoked it.
The same Call shape reaches an operation whether it was invoked over HTTP, GraphQL or directly from Go, which is what lets one operation serve every entry point. Effects is private to this call: its contents reach the request only if the operation and every after hook succeed.
func (*Call) Annotate ¶
Annotate records a fact about this call for its After and Finally hooks, such as the organization an operation resolved and acted on, which its input does not carry. Last write wins. The value lives on this Call, not in the transaction, so a Finally hook still reads it after a failure rolled the writes back. Values are plain strings on purpose: a hook reads them into a log, so nothing that is not meant to be logged should go in, and a secret never should. An operation called from inside another gets its own Call, so it cannot overwrite the outer call's annotations.
func (*Call) Annotation ¶
Annotation returns the value Annotate recorded under key, or "".
func (*Call) Cookie ¶
Cookie returns the value of plugin cookie name as the client sent it, and whether it sent one. A Go call, or a transport without cookies, has none.
func (*Call) Header ¶
Header reads a request header, or returns "" when the call has no transport headers, such as a direct Go call.
func (*Call) RedirectOnError ¶
RedirectOnError makes a failure of this call, from here on, answer with a redirect to url carrying the error code ("?error=<code>", or "&error=" when url has a query) instead of a JSON error, for a browser flow such as an OAuth callback whose user is on the app's page, not in a script. The error is still returned to a Go caller; the transport sees the redirect in RequestEffects and applies it. Nothing else the failed call recorded reaches the request.
An empty url takes WithErrorURL's page, as better-auth's callbacks fall back to onAPIError.errorURL; with neither, the call answers JSON as before.
func (*Call) RequireOwner ¶
RequireOwner checks that row id of o.Table belongs to the call's user, as better-auth's requireResourceOwnership middleware does, in one indexed read on the call's connection (the operation's transaction, when it has one): a signed-out call is ErrSignedOut, an empty id ErrBadRequest, a missing row o.NotFound, and another user's row o.Forbidden or, by default, o.NotFound. The owner is compared as stored, exactly, as every id is.
It is for an operation's own rows (a passkey, a key, a device); a plugin that also needs the row's other columns calls RequireOwnerRow.
func (*Call) RequireOwnerRow ¶
func (c *Call) RequireOwnerRow(ctx context.Context, o Ownership, id string, cols ...sqlb.Binding) error
RequireOwnerRow is RequireOwner that also reads the row: cols are bound to the columns of row id of o.Table and filled when, and only when, the row belongs to the call's user: better-auth's verifiedResource. The read keys on the id and the owner together, so another user's values never reach cols; a miss is then classified by RequireOwner's own read (missing, another user's), with the same answers. A signed-out call, an empty id or no columns is refused before any read.
func (*Call) Respond ¶
Respond answers the call from a Before hook with out, as better-auth's before hook returning a response (ctx.json) does: the remaining Before hooks, the operation and every After hook are skipped, the effects the hooks recorded still apply, and a mutation's transaction still commits. out must have the operation's output type, or the call fails with ErrBadOperation, so a Go caller never gets a zero result it did not ask for. A hook that refuses returns an error instead; Respond is for a success the app produces itself (a canned answer in a maintenance window, a result served from the app's own cache). Called from an After hook or the operation it changes nothing.
func (*Call) SetInput ¶
SetInput replaces the input the operation runs with, from a Before hook, as better-auth's before hook returning { context } does: normalising a field, filling a default the app decides. v must have the operation's input type (the type its Input holds), or the call fails with ErrBadOperation before the operation runs, so a wrong type is a loud error and never a zero input. Later hooks see the new value in Input.
What it does not redo: the per-account rate limit (ByIdentifier) was counted on the decoded input before any hook ran, so a hook cannot move a caller onto another account's budget or off its own.
func (*Call) SetSignedCookie ¶
SetSignedCookie is SetCookie with the value signed, as better-auth's setSignedCookie is: base64url of the value, a dot, and an HMAC-SHA256 over the cookie's name and value under the key WithSecret derives for KeyLabelCookies. The client can read the value's encoding but not change it, and SignedCookie refuses any value it did not sign. Without WithSecret it is ErrNoSecret.
func (*Call) SignInProven ¶
func (c *Call) SignInProven(ctx context.Context, kind IdentifierKindName, raw string, create bool) (ProvenSignIn, error)
SignInProven signs in the verified owner of the identifier (kind, raw), whose control the calling operation has just proven by checking a code or a link it sent there. With no verified owner and create set, it creates a user holding that identifier, verified and primary. It records the session on c.Effects (IssueSessionOf), so the transport sets the cookie or bearer token, and it creates the session through CreateSession, so a user's second factor still applies.
It exists because every "prove you control this address" sign-in — an emailed code, an SMS code, a magic link — ends in these same steps, and a copy per plugin is where one of them forgets that only a VERIFIED owner may be signed in: an unverified row is a stranger's claim, never an account.
With no verified owner and create false it returns ErrIdentityNotFound. The caller must map that to the same error a wrong code or link gets (ErrCodeInvalid, ErrTokenInvalid), so the answer does not tell a stranger which identifiers have accounts. ctx carries the tenant scope that FindIdentity and CreateIdentity use, and the operation's transaction, so a failure after the user is created leaves no user behind.
func (*Call) SignInProvenAs ¶
func (c *Call) SignInProvenAs(ctx context.Context, kind IdentifierKindName, raw string, newUser *NewUser) (ProvenSignIn, error)
SignInProvenAs is SignInProven with the profile a created user gets: newUser nil means "do not create" (ErrIdentityNotFound for an unknown identifier), and a non-nil value is used only when this call creates the user, so a first sign-in by code or link can carry the name the person typed. An existing user's profile is never touched: the input of a sign-in is not a place to edit an account from.
func (*Call) SignedCookie ¶
SignedCookie returns the value of a cookie SetSignedCookie set, and false when the client sent none, or one whose signature does not verify under any WithSecret key (the current one or one before a rotation), compared in constant time.
type Client ¶
type Client struct {
IP string
UserAgent string
// Origin is the scheme and host the request claims to come from, taken
// from the Origin header or, when it is missing, from the Referer.
Origin string
// Locale is the language the client prefers, as a BCP 47 tag such as
// "de-CH", or "". Messages sent during the request carry it.
Locale string
// FetchSite is the request's Sec-Fetch-Site header as the browser sent
// it ("same-origin", "same-site", "cross-site", "none"), or "" when the
// client sent none. Browsers set it and scripts cannot change it, which
// is what makes WithFetchMetadata a defence; a value an app made up
// would defend nothing.
FetchSite string
}
Client describes the other end of a request. Every field comes from the client and is information only: none of them may decide anything about security except Origin, which is compared against a fixed allow-list.
type Closer ¶
type Closer interface {
Close() error
}
Closer releases resources when the app shuts down.
type CodeOptions ¶
type CodeOptions struct {
// Reuse returns the open code of purpose for subject again, with its
// expiry moved to ttl from now, instead of replacing it: better-auth's
// resendStrategy "reuse". A code with no attempts left (MaxAttempts) or
// past its expiry is replaced as usual, and a resend never refills the
// attempt budget. It needs WithSecret (ErrNoSecret otherwise).
//
// The core's digits are derived, never stored readable: HMAC-SHA256 of
// the row id under the key derived from WithSecret under KeyLabelCodes,
// reduced to CodeLength digits. A Generate code cannot be recomputed, so
// it is also stored sealed (AES-256-GCM, bound to its row id) in
// auth_code.code_sealed under the keys derived from every WithSecret
// secret, current first. Either way, anyone holding the database AND the
// secret can read every open code. A row whose code does not recompute
// or open (issued without Reuse, or before every listed secret) gets a
// fresh code.
Reuse bool
// MaxAttempts is the attempt budget Reuse checks the open code against;
// 0 means DefaultCodeMaxAttempts. Use the value CheckCode is given.
MaxAttempts int
// Generate makes the code instead of the core's random digits. The
// caller checks its strength; an empty code, or one longer than
// MaxGeneratedCodeLength, is ErrBadOperation.
Generate func(ctx context.Context) (string, error)
}
CodeOptions changes how IssueCodeWith makes a code. The zero value is IssueCode: fresh random digits, replacing any open code.
type ColumnExtender ¶
type ColumnExtender interface {
Columns() []ColumnExtension
}
ColumnExtender adds columns to core tables.
type ColumnExtension ¶
type ColumnExtension struct {
// Table is the core table to extend. Only auth_user is extendable: it is
// the one row every request loads.
Table string
// Name must match KindNamePattern and should carry the plugin's name, as
// "two_factor_enabled", so it cannot collide with another plugin's.
Name string
Type ColumnType
Nullable bool
// Default is a constant SQL literal such as "”" or "false", or empty for
// none. A NOT NULL column needs one, so existing rows can be filled.
Default string
Unique bool
// contains filtered or unexported fields
}
ColumnExtension is a column a plugin adds to a core table.
It exists so a flag such as "two-factor is on" costs no join: the column is a flat field of the user. The plugin owns it — only that plugin may write it, through SetUserColumn — and New refuses two plugins that add one column.
type ColumnType ¶
type ColumnType string
ColumnType is the portable type of a plugin column. The core maps it to each engine's spelling in the generated migration.
const ( ColumnText ColumnType = "text" ColumnTimestamp ColumnType = "timestamp" ColumnBool ColumnType = "bool" ColumnInt ColumnType = "int" )
Portable column types. Deliberately few: a plugin column is a flag, a timestamp, a short string or a number. Anything larger belongs in the plugin's own table.
type Config ¶
type Config struct {
// Dialect renders every statement.
Dialect sqlb.Dialect
// Identifiers is every kind the plugins registered, in plugin order.
Identifiers []IdentifierKind
// BaseURL is the app's public URL. Its origin is trusted, and plugins
// build the links they send from it.
BaseURL string
// TrustedOrigins are extra origins allowed to send cookie-authenticated
// mutations, as "scheme://host".
TrustedOrigins []string
// FreshWindow is how recent a session must be for FreshSignIn operations.
FreshWindow time.Duration
// Session holds the session lifetimes.
Session SessionConfig
// PendingSessionTTL is how long a pending second-factor session lives;
// see WithPendingSessionTTL.
PendingSessionTTL time.Duration
// CodeLength is the number of digits of a one-time code; see
// WithCodeLength.
CodeLength int
// MultiSession is the WithMultiSession setting; Enabled is false
// without it.
MultiSession MultiSessionConfig
// DefaultPageSize is the WithDefaultPageSize value; 0 leaves each
// list's own default. Lists read it through PageSize.
DefaultPageSize int
// ErrorURL is the WithErrorURL page a failed browser flow lands on
// when the flow names none; empty answers such failures as JSON.
ErrorURL string
// DeliveryOutbox is whether WithDeliveryOutbox is on: messages are stored
// in the transaction that queues them and retried until sent. A plugin
// whose step must not happen without its message (plugin/stale's
// deletion) checks it in Validate.
DeliveryOutbox bool
// contains filtered or unexported fields
}
Config is the resolved configuration, visible to plugins in Validate. It is read-only by convention and returned by value from Auth.Config, so a mutation cannot reach the running instance.
func (Config) CanDeliver ¶
func (c Config) CanDeliver(name IdentifierKindName) bool
CanDeliver reports whether a message can be sent to an identity of kind name: the kind is registered and its plugin implements Deliverer, so Auth.Deliver will not answer ErrNoDeliverer for it. A plugin that sends to a kind the app chooses (an invitation addressed by InviteBy) asks it in Validate, so a kind nobody can deliver to fails auth.New instead of the first send. It says nothing about whether a send will succeed: a deliverer's mailer or SMS gateway can still fail.
func (Config) DeriveKey ¶
DeriveKey returns the SealKeySize-byte key for label, derived from the current WithSecret secret with HKDF-SHA256: the key that seals. The same secret and label always give the same key, and different labels give unrelated keys. label is one of the KeyLabel constants or a plugin's own fixed constant, never built from input: a label an attacker chooses is a key an attacker chooses. The error is ErrNoSecret when there is no secret.
func (Config) DeriveKeys ¶
DeriveKeys returns the key for label from every secret, current first: exactly the argument list NewKeyRing takes, so a plugin that seals its own values builds its ring with NewKeyRing(keys[0], keys[1:]...) and opens values sealed before a secret rotation.
func (Config) HasIdentifier ¶
func (c Config) HasIdentifier(name IdentifierKindName) bool
HasIdentifier reports whether a kind is registered.
func (Config) HasSecret ¶
HasSecret reports whether WithSecret was given, so a plugin's Validate can say "pass a key or a secret" precisely.
func (Config) Identifier ¶
func (c Config) Identifier(name IdentifierKindName) (IdentifierKind, bool)
Identifier returns a copy of the registered kind. A copy, so a plugin that reads the configuration cannot change the running policy.
func (Config) LoginKinds ¶
func (c Config) LoginKinds() []IdentifierKind
LoginKinds returns the kinds allowed in sign-in, in plugin order. A single sign-in field tries them in this order.
func (Config) PageSize ¶
PageSize is the number of rows a list answers: requested when it is positive and at most limit; limit when requested is larger; and with no request (0 or less), WithDefaultPageSize when set, else fallback, the list's own default, both capped at limit. A list calls it with its own fallback and limit, so one rule decides every list's size.
func (Config) RecoveryKinds ¶
func (c Config) RecoveryKinds() []IdentifierKind
RecoveryKinds returns the kinds allowed to receive recovery messages.
type ConfirmDeleteAccountRequest ¶
type ConfirmDeleteAccountRequest struct {
Token WireToken `json:"token"`
}
ConfirmDeleteAccountRequest is the input of core.account-delete-confirm.
type Credential ¶
type Credential string
Credential names how the client presented its session token.
const ( CredentialNone Credential = "none" CredentialCookie Credential = "cookie" CredentialBearer Credential = "bearer" )
How a token can arrive. The origin check applies only to CredentialCookie, because a browser attaches a cookie on its own and never a bearer header.
func RequestCredential ¶
func RequestCredential(ctx context.Context) Credential
RequestCredential reports how the request in ctx presented its token, so a transport can apply an issued session the same way it arrived.
type CredentialCounter ¶
type CredentialCounter interface {
// SignInCredentials counts userID's credentials in this plugin, leaving
// out the one named by exceptID when it is this plugin's.
SignInCredentials(ctx context.Context, userID, exceptID string) (int, error)
}
CredentialCounter is a plugin that holds ways into an account that are not identities, such as linked social accounts. The core asks it before removing anything, so no plugin can strand a user.
type CredentialListener ¶
CredentialListener is a plugin that must hear when a user's credential changed: a password reset or change, a recovery. The two-factor plugin forgets the user's remembered devices on it, because a reset is often the answer to a stolen account and a device trusted before it must not outlive it.
type DataHooker ¶
type DataHooker interface {
DataHooks() DataHooks
}
DataHooker supplies data hooks.
type DataHooks ¶
type DataHooks struct {
User TableHooks[User]
Identity TableHooks[Identity]
Session TableHooks[Session]
Verification TableHooks[Verification]
// SessionEnded runs for every session that ends: sign-out, revoke, an
// admin, a ban, a password change, token-reuse theft, user deletion and
// the expiry sweep (see SessionEndReason), once per session, inside the
// transaction that ends it and before the store is told, so a rollback
// leaves nothing durable. tx is the transaction and must be used for
// writes. A hook cannot refuse an ending; failures are reported, not
// enforced: a session that must end (a stolen token, a banned user, a
// changed password) never stays alive because a hook failed. When a hook
// returns an error the core rolls that attempt back, ends the session
// again without the hooks in a transaction of its own, and emits
// SecuritySessionEndHookFailed (and logs it) with the reason and error.
// Inside a transaction the caller already opened the attempt cannot be
// rolled back alone, so the error is reported and the ending continues,
// and the hook's own writes in that transaction stay with it. Gaps: a store that expires
// sessions itself (Redis TTL, storage) or wraps another (mirror) reports
// no expiry, and stateless mode reports only ends the core performs
// (see SessionEnd). It is not told of a pending second-factor session
// being replaced (SessionLifecycle.SessionReplaced is).
SessionEnded func(ctx context.Context, tx sqlb.DBTX, e SessionEnd) error
}
DataHooks are a plugin's hooks on the core tables.
type DeleteAccountResult ¶
type DeleteAccountResult struct {
// Deleted is true when the account is gone; the session is cleared.
Deleted bool `json:"deleted"`
// ConfirmationSent is true when a link went out instead
// (AccountDeletion.ConfirmByLink); the account exists until it is spent.
ConfirmationSent bool `json:"confirmationSent"`
}
DeleteAccountResult is the output of core.account-delete. Exactly one of the two fields is true.
type Deliverer ¶
Deliverer is implemented by an identifier plugin that can send a message to one of its identities. A kind with CanRecover must come from a Deliverer.
type Dependent ¶
type Dependent interface {
Requires() []string
}
Dependent names plugins that must also be registered.
type DeviceRecognizer ¶
type DeviceRecognizer interface {
KnownDevice(ctx context.Context, in DeviceSignIn) (bool, error)
}
DeviceRecognizer decides whether a sign-in comes from a device the user has used before. It runs after the sign-in, off the request, so it may read the database; an error is logged and nothing is sent, since an alarm on every sign-in while a store is down would teach users to ignore it. The default is SessionDevices; an app with a device cookie, a fingerprinting service or a table of its own passes its rule through WithDeviceRecognizer.
func SessionDevices ¶
func SessionDevices() DeviceRecognizer
SessionDevices returns the default DeviceRecognizer: a sign-in is from a known device when another of the user's sessions has the same DeviceKey (the user agent without its version numbers, so a browser update is not a new device). It reads the live sessions and, under WithSessionArchive, the revoked ones, so a device the user signed out of stays known. Pending sessions (a second factor not yet passed) and impersonation sessions are not the user's devices and do not count. A user with no other session at all (the first sign-in, or every session expired and swept) is treated as known: there is nothing to compare with, and a mail on every first sign-in would be noise. The IP address is not compared: it changes with every network a phone joins.
It needs a session list, so New refuses it with WithStatelessSessions, where only the request's own session is known.
type DeviceRecognizerFunc ¶
type DeviceRecognizerFunc func(ctx context.Context, in DeviceSignIn) (bool, error)
DeviceRecognizerFunc adapts a function to DeviceRecognizer.
func (DeviceRecognizerFunc) KnownDevice ¶
func (f DeviceRecognizerFunc) KnownDevice(ctx context.Context, in DeviceSignIn) (bool, error)
KnownDevice calls f.
type DeviceSessionRequest ¶
type DeviceSessionRequest struct {
ID string `json:"id"`
}
DeviceSessionRequest names a session the device holds, for switch and revoke.
type DeviceSessionView ¶
type DeviceSessionView struct {
// ID is the session id, what switch and revoke take.
ID string `json:"id"`
User UserView `json:"user"`
// Active is true for the session the device is using now.
Active bool `json:"active"`
ExpiresAt time.Time `json:"expiresAt"`
// Extras is what the WithSessionExtras function computed for this
// session, under WithDeviceSessionExtras; absent otherwise. It is any
// because SessionExtras returns the app's own JSON-encodable shape, which
// the library never reads, only encodes.
Extras any `json:"extras,omitempty"`
}
DeviceSessionView is one account signed in on the device.
type DeviceSessionsResult ¶
type DeviceSessionsResult struct {
Sessions []DeviceSessionView `json:"sessions"`
}
DeviceSessionsResult is the output of core.device-sessions: the active session first, then the others in the order the device holds them.
type DeviceSignIn ¶
type DeviceSignIn struct {
// Auth is the instance, for a recognizer that reads sessions or tables.
Auth *Auth
UserID string
// Session is the new session; it is already committed, so a recognizer
// listing sessions must leave it out by id.
Session Session
Client Client
// Operation and Method are as in SignInAttempt: the operation that
// signed the user in and its plugin, "" for a Go call outside one.
Operation EndpointID
Method string
}
DeviceSignIn is the sign-in a DeviceRecognizer judges. It is a struct so fields can be added without breaking implementers.
type Done ¶
type Done struct {
OK bool `json:"ok"`
}
Done is the output of operations that return nothing but success.
type Effect ¶
type Effect struct {
Kind EffectKind
Token Token
ExpiresAt time.Time
URL string
Transient bool
Value string
// UserID and SessionID name the session an EffectIssueSession is for,
// set by IssueSessionOf; IssueSession leaves them empty. Transports do
// not need them; an audit hook reading RequestEffects does, since a
// public sign-in's Call has no User.
UserID string
SessionID string
// Name is the response header an EffectHeader sets, or the plugin
// cookie an EffectCookie sets.
Name string
// Replaces is set on an EffectIssueSession whose token takes the place
// of the one the request presented, for the same sign-in: a refresh
// that changed the token (WithTokenRotation, a resealed stateless
// token), a second-factor promotion, a completed session task. A
// multi-session transport then drops the old token instead of keeping
// it as another account on the device, where it would come back as a
// replaced token and, under WithTokenRotation, revoke the session.
Replaces bool
}
Effect is one recorded effect.
Which fields are set depends on Kind, and each transport must read only the fields of the kind it is handling:
- EffectIssueSession: Token (the raw session token), ExpiresAt, Transient (the cookie must have no expiry, so it ends with the browser) and Replaces (the token replaces the request's own, for the same sign-in, rather than signing in another account).
- EffectClearSession: no fields.
- EffectRedirect: URL.
- EffectBindFlow: Token and ExpiresAt; an empty Token removes the binding.
- EffectCacheSession: Value (opaque, not a secret on its own but tied to the session token) and ExpiresAt, when the value stops being used.
- EffectClearCache: no fields.
- EffectForgetDeviceSession: Token (the one to drop from the device's set).
- EffectRememberDevice: Token and ExpiresAt; an empty Token removes the device token.
- EffectHeader: Name (the header) and Value.
- EffectCookie: Name (the plugin cookie, CheckCookieName), Value and ExpiresAt; a zero ExpiresAt deletes the cookie.
func RequestEffects ¶
RequestEffects returns the effects that succeeded operations recorded during the request in ctx, for the transport to apply. It returns nil outside a request.
type EffectKind ¶
type EffectKind string
EffectKind names what an operation asks the transport to do after it succeeds.
const ( EffectIssueSession EffectKind = "issue_session" EffectClearSession EffectKind = "clear_session" EffectRedirect EffectKind = "redirect" // EffectBindFlow ties a multi-step flow, such as an OAuth sign-in, to // this client, so a flow started by one browser cannot be finished in // another. That is the defence against login CSRF, where an attacker gets // a victim signed in to the attacker's account. EffectBindFlow EffectKind = "bind_flow" // EffectCacheSession hands the client a value to send back as // Request.CachedSession, so its next requests skip the session lookup // (the snapshot SessionCache). EffectClearCache removes it: the session // ended on this device, and a copy that kept answering for it would // undo the sign-out for as long as it lived. EffectCacheSession EffectKind = "cache_session" EffectClearCache EffectKind = "clear_cache" // EffectForgetDeviceSession asks the transport to drop one token from // the device's set of signed-in accounts (WithMultiSession): the // account was signed out on this device, or its token stopped opening. // An issue-session effect for a token the device already holds is the // opposite: that account becomes the active one. EffectForgetDeviceSession EffectKind = "forget_device_session" // EffectRememberDevice hands the client a device token to send back as // Request.Device until ExpiresAt, for a second-factor plugin's // "remember this browser"; an empty Token forgets it. EffectRememberDevice EffectKind = "remember_device" // EffectHeader hands the client a value in a response header of the // plugin's choosing, such as the jwt plugin's Set-Auth-JWT on the session // read. A plugin owns its header name; the transport also lists it in // Access-Control-Expose-Headers, because a header a cross-origin script // cannot read is a header that was never sent as far as it can tell. EffectHeader EffectKind = "header" // EffectCookie hands the client a plugin cookie (Effects.SetCookie): // the transport names it beside its own cookies and gives it their // fixed attributes; the plugin chooses only the name, the value and // the expiry. EffectCookie EffectKind = "cookie" )
The effects an operation can record.
Why effects exist at all: an operation such as sign-in must end with a cookie, but an operation that writes an HTTP header could not run from a CLI, a job or a GraphQL resolver that has no response writer. So the operation records what it wants, and each transport decides what that means for its protocol — a Set-Cookie header, a token field, a returned URL.
type Effects ¶
type Effects struct {
// contains filtered or unexported fields
}
Effects collects the effects of one call, or of one request.
It is safe for concurrent use, because a GraphQL request resolves several fields at once and each field's operation adds to the same request's set.
func (*Effects) BindFlow ¶
BindFlow asks the transport to remember t for this client until expiresAt, and to present it as Request.Flow on later requests. An empty t forgets it.
func (*Effects) CacheSession ¶
CacheSession hands the client v to send back as Request.CachedSession until expiresAt.
func (*Effects) ClearCache ¶
func (e *Effects) ClearCache()
ClearCache asks the transport to forget the client's cached session value.
func (*Effects) ClearSession ¶
func (e *Effects) ClearSession()
ClearSession asks the transport to remove the client's session token.
func (*Effects) ForgetDeviceSession ¶
ForgetDeviceSession asks the transport to drop t from the device's set of signed-in accounts; see EffectForgetDeviceSession.
func (*Effects) Header ¶
Header asks the transport to send value in the response header name and to let a cross-origin script read it; see EffectHeader. A transport with no headers (a CLI, a job) ignores it, and a Go caller reads it from RequestEffects.
func (*Effects) IssueSession ¶
IssueSession asks the transport to hand a new session token to the client.
func (*Effects) IssueSessionOf ¶
IssueSessionOf hands a session's token to the client with the session's own expiry and "remember me" choice. Prefer it to IssueSession, which always asks for a persistent cookie.
func (*Effects) Redirect ¶
Redirect asks the transport to send the client to url. HTTP transports answer with a 302; others return the URL.
func (*Effects) RememberDevice ¶
RememberDevice asks the transport to keep t as the client's device token until expiresAt and present it as Request.Device on later requests; an empty t forgets it. See EffectRememberDevice.
func (*Effects) ReplaceSessionOf ¶
ReplaceSessionOf is IssueSessionOf for a token that replaces the request's own for the same sign-in; see Effect.Replaces.
func (*Effects) SetCookie ¶
SetCookie records a plugin cookie for the transport to set: name (see CheckCookieName), value, and expiresAt, when the browser drops it; a zero expiresAt deletes the cookie. Like every effect it reaches the client only if the call succeeds. The value is readable by nobody but the server (HttpOnly) but is NOT protected from the client changing it: anything the server must trust goes through Call.SetSignedCookie.
type EndpointID ¶
type EndpointID string
EndpointID names an operation, as "<plugin>.<name>". Plugins export theirs as constants so another plugin's hook can name them without a bare string.
const ( // EndpointDeleteAccount deletes the signed-in user's account, or sends // the confirmation link when WithAccountDeletion asks for one. EndpointDeleteAccount EndpointID = "core.account-delete" // EndpointConfirmDeleteAccount spends the confirmation link and deletes // the account it was issued for. EndpointConfirmDeleteAccount EndpointID = "core.account-delete-confirm" )
Account deletion operation ids.
const ( EndpointDeviceSessions EndpointID = "core.device-sessions" EndpointSwitchDeviceSession EndpointID = "core.device-sessions-switch" EndpointRevokeDeviceSession EndpointID = "core.device-sessions-revoke" )
Multi-session operation ids.
const ( EndpointSession EndpointID = "core.session" EndpointSignOut EndpointID = "core.sign-out" EndpointSignOutAll EndpointID = "core.sign-out-all" EndpointSessions EndpointID = "core.sessions" EndpointRevokeSession EndpointID = "core.sessions-revoke" EndpointProfile EndpointID = "core.profile" // EndpointRefreshSession slides the expiry on request, under // WithDeferredSessionRefresh; see that option. EndpointRefreshSession EndpointID = "core.session-refresh" )
Core operation ids.
type Error ¶
Error is an error that a caller outside the process may see.
Why it exists: an HTTP client needs a stable code to pick a message, a Go caller needs a value for errors.Is, and neither may ever see an internal detail such as a SQL error, because that detail tells an attacker how the system is built. An Error carries exactly the three things that are safe to send — a code, a status and a message — and every transport renders it the same way. Anything that is not an Error is reported as "auth.internal" and its text goes to the log only.
Invariant: Code is "<plugin>.<name>" or "auth.<name>" for the core, so two plugins can never declare the same code. Message is written for an end user and must not depend on whether an account exists.
func Errors ¶
func Errors() []*Error
Errors returns every error declared with NewError so far, one per code, the first declaration, sorted by code: the complete list a client can meet, for a generated reference or typed client. It is complete once every plugin package is imported, which is the case for any program that wires them.
func NewError ¶
NewError declares an error once, at package level, in the plugin that owns it. The value is then both the sentinel for errors.Is and the source of the wire response.
type ErrorInfo ¶
type ErrorInfo struct {
Code string `json:"code"`
Status int `json:"-"`
Message string `json:"message"`
// OriginalMessage is the declared English message when Message was
// translated by the WithMessages catalogue, as better-auth's i18n plugin
// answers it, so a log or a support screen can show the text the
// reference documents; empty (and left out of the JSON) when Message is
// the declared one.
OriginalMessage string `json:"originalMessage,omitempty"`
}
ErrorInfo is the safe, transport-neutral description of any error.
func ErrorInfoFor ¶
ErrorInfoFor is Auth.ErrorInfo for a transport that holds only the request context: the request attached by WithRequest knows its Auth. With no request in ctx it is ErrorOf, the declared message.
func ErrorOf ¶
ErrorOf describes err for a response. Every transport calls it, so REST, GraphQL and any custom transport answer the same failure with the same code.
An error that is not an *Error — a database error, a bug — becomes ErrInternal. That is deliberate: its text may contain SQL, a table name or a value, and none of that may reach a client. The caller is expected to log the original error before it discards it.
type ExclusiveVerificationStore ¶
type ExclusiveVerificationStore interface {
VerificationStore
RefusesDuplicateTokens() bool
}
ExclusiveVerificationStore is a VerificationStore that can promise exclusive inserts: when RefusesDuplicateTokens reports true, Insert of a hash already stored (live or not yet swept) fails with ErrTokenExists, and of any number of concurrent Inserts of one hash exactly one succeeds. The SQL store always does (the token_hash column is unique); the key-value store does when its storage is a SetIfAbsenter. A caller that needs a single-use claim asks this before relying on it, because a store without the promise would accept the second claim silently.
type Fields ¶
type Fields map[string]json.RawMessage
Fields is the "fields" object of a sign-up body: column name to JSON value. A value is decoded by the column's declared type: a JSON string for text, an integer for int, true or false for bool, an RFC 3339 string for timestamp, and null for a nullable column.
func SplitBody ¶
SplitBody decodes a request body whose sign-up fields sit at the top level, as better-auth puts additionalFields beside email and password. The keys dst declares (its json names, matched case-insensitively as encoding/json does) are decoded into dst strictly, unknown nested keys and wrong types included; every other top-level key is returned as Fields for MergeFields to check. A transport's UnmarshalJSON calls it with an alias of its own type, so the alias has no UnmarshalJSON to recurse into. dst is any for the reason json.Unmarshal's is: the target is the caller's struct, and anything but a pointer to a struct is refused with errSplitTarget.
type GeoLocation ¶
type GeoLocation struct {
// Country is the ISO 3166-1 alpha-2 code ("DE"), upper-case, or "" when
// only coordinates are known.
Country string
// Latitude and Longitude are degrees, valid only when HasCoordinates.
Latitude, Longitude float64
// HasCoordinates is whether Latitude and Longitude mean anything; a
// country-only database leaves it false and so cannot judge travel.
HasCoordinates bool
}
GeoLocation is where an address is, as far as a GeoLocator knows.
type GeoLocator ¶
type GeoLocator interface {
Locate(ctx context.Context, ip netip.Addr) (loc GeoLocation, found bool, err error)
}
GeoLocator turns a client address into a location. found is false for an address the source does not know (a private range, a gap in the database); an error is a source that failed (a database that cannot be read). The library treats the two differently: see GeoRules.DenyUnknown.
It runs on the sign-in request's goroutine, so it must be fast and local (an in-memory database, not a network call without a tight timeout).
type GeoLocatorFunc ¶
GeoLocatorFunc adapts a function to GeoLocator.
func (GeoLocatorFunc) Locate ¶
func (f GeoLocatorFunc) Locate(ctx context.Context, ip netip.Addr) (GeoLocation, bool, error)
Locate calls f.
type GeoRules ¶
type GeoRules struct {
// Allow, when not empty, lists the only countries that may sign in (ISO
// 3166-1 alpha-2, any case). Deny lists countries that may not; a country
// in both is denied.
Allow, Deny []string
// DenyUnknown applies Action to an address the locator does not know
// (found false). Off by default: private networks, a development machine
// and gaps in the database would otherwise lock people out. A locator
// that returns an error is never denied for it: the check fails open and
// logs, because a GeoIP outage must not be a sign-in outage.
DenyUnknown bool
// Action is what a violation does: RateBlock (the default, 403
// auth.geo_blocked), RateLog (record only) or RateChallenge (the
// RateChallenger decides, as for a rate rule).
Action RateAction
}
GeoRules are the country rules for sign-in. A sign-in operation whose client address resolves to a country in Deny, or outside a non-empty Allow, gets Action.
type GetAndDeleter ¶
type GetAndDeleter interface {
SecondaryStorage
// GetAndDelete returns the value under key and removes it, atomically;
// false when there is none or it expired.
GetAndDelete(ctx context.Context, key string) (string, bool, error)
}
GetAndDeleter is a SecondaryStorage that can read and delete one key in a single atomic step: better-auth's secondaryStorage.getAndDelete, Redis GETDEL. It is what makes a verification token in a key-value store single-use: a Get then a Delete would let two requests both read the token before either deleted it.
type HookStage ¶
type HookStage string
HookStage names which function of an OperationHook a hook span wraps.
type IPReputation ¶
IPReputation judges a client address. It runs on the request's goroutine before the credentials are read, so it must be fast (a local list, a cache in front of a lookup service with a tight timeout). The address is never private-range-filtered: the check decides what an unknown address means.
type IPReputationFunc ¶
IPReputationFunc adapts a function to IPReputation.
func (IPReputationFunc) CheckIP ¶
func (f IPReputationFunc) CheckIP(ctx context.Context, ip netip.Addr) (RiskVerdict, error)
CheckIP calls f.
type Identified ¶
type Identified interface {
RateIdentifier() string
}
Identified is an operation input that names the account it is about, for ByIdentifier rules.
type IdentifierKind ¶
type IdentifierKind struct {
// Name is written to auth_identity.kind and to generated index names, so
// it must match KindNamePattern.
Name IdentifierKindName
// Unique requests a unique index over the normalized value, among rows of
// this kind.
Unique bool
// UniqueOnlyVerified narrows that index to verified rows. It is the right
// setting for anything a stranger can type: an unverified claim then
// grants nothing and blocks nobody.
UniqueOnlyVerified bool
// ScopedByTenant makes uniqueness hold per scope_id instead of globally.
ScopedByTenant bool
// CanLogin allows this kind in a sign-in lookup.
CanLogin bool
// CanRecover allows this kind to receive account-recovery messages. The
// plugin that registers it must implement Deliverer.
CanRecover bool
// Verifiable means the kind has a proof-of-control flow. A handle has none.
Verifiable bool
// RequireVerified refuses a sign-in through an unverified identity of
// this kind. Sign-in methods check it after the credential, so the
// refusal reveals nothing to a caller who does not know the password.
RequireVerified bool
// Normalize converts input to the one form that is stored and compared.
Normalize func(raw string) (string, error)
// Validate rejects a normalized value that is malformed. Optional.
Validate func(normalized string) error
}
IdentifierKind is the policy for one way of naming a user. Identifier plugins build one and return it from Identifiers; the core stores every kind in the one auth_identity table and applies these rules to all of them the same way.
Why uniqueness is policy and not schema: whether an email must be unique is a product decision. A phone-first app may let a family share an address; a developer tool may allow many addresses per account. Neither fits a fixed UNIQUE column.
Invariant: a kind that signs in or recovers must be unique, and a unique kind must have a normalizer. New refuses anything else, because each violation is a deployment that is broken in a way nobody notices until a user is stuck.
func ApplyKind ¶
func ApplyKind(k IdentifierKind, opts ...KindOption) IdentifierKind
ApplyKind runs options over a base kind. Plugins call it from their constructors.
type IdentifierKindName ¶
type IdentifierKindName string
IdentifierKindName is the discriminator stored in auth_identity.kind, such as "email" or "phone".
The core defines no kinds of its own: identifier plugins register them. It is a named type rather than a string so a kind cannot be confused with an identifier's value, which is also a string and appears beside it in almost every signature.
type IdentifierProvider ¶
type IdentifierProvider interface {
Identifiers() []IdentifierKind
}
IdentifierProvider is implemented by a plugin that registers identifier kinds: email, phone, username.
type Identity ¶
type Identity struct {
ID string
UserID string
// Kind selects which registered kind's rules apply: its normalizer, its
// uniqueness, whether it can sign in or recover.
Kind IdentifierKindName
// Value is the normalized form, and the only form any lookup, comparison or
// index uses. It is compared exactly, never through SQL lower(): the
// normalizer is the one folding rule, and a second one in SQL would differ
// between engines.
Value string
// ValueRaw is what the user typed, kept to show it back. Never compared.
ValueRaw string
// VerifiedAt is nil until the user proved control of Value.
//
// The most security-critical nullable in the schema. Any flow that treats
// an unverified identity as proof of ownership — matching an invitation,
// linking a social account, joining by domain — is an account takeover,
// and the published advisories in comparable libraries are all that one
// mistake. Match identities to people only through VerifiedIdentities.
VerifiedAt *time.Time
// IsPrimary marks the one identity per kind that the kind's plugin copies
// onto the user row. Only a verified identity can be primary.
IsPrimary bool
// ScopeID confines uniqueness to one tenant. Nil means the value is unique
// across the deployment.
ScopeID *string
CreatedAt time.Time
}
Identity is one way of naming a user: an email address, a phone number, a handle. Social accounts are not identities; the oauth plugin keeps them in its own table, because a provider asserts them rather than the user.
type ImportIdentity ¶
type ImportIdentity struct {
Kind IdentifierKindName
// Value is what the other system held; it is normalized like typed
// input.
Value string
// Verified imports the identity as proven. An import of addresses the
// other system had verified sets it; an unverified identity is a claim
// the user must still prove here.
Verified bool
// Primary makes it the primary identity of its kind; needs Verified.
Primary bool
}
ImportIdentity is one identifier of an imported user.
type ImportResult ¶
type ImportResult struct {
Index int
// User is set when the row was imported.
User User
// Err is why it was not: ErrUserExists, ErrIdentityTaken,
// ErrIdentifierInvalid, a hook's refusal, or a database error. The row
// left nothing behind.
Err error
}
ImportResult is the outcome for one ImportUser, in input order.
type ImportUser ¶
type ImportUser struct {
// ID keeps the other system's id when it fits (opaque text, unique);
// empty makes a new one. Keeping ids lets the app's own rows keep their
// foreign keys.
ID string
Name string
Image string
// CreatedAt keeps the original sign-up time; zero means now.
CreatedAt time.Time
Identities []ImportIdentity
}
ImportUser is one user to import.
type ImpossibleTravelRule ¶
type ImpossibleTravelRule struct {
// MaxSpeedKmh is the fastest believable journey in km/h; 0 takes
// DefaultTravelSpeedKmh. A negative or non-finite value is refused.
MaxSpeedKmh float64
// MinDistanceKm is the distance below which two locations count as the
// same place, whatever the time: GeoIP is accurate to tens or hundreds of
// kilometres, and a carrier's address can resolve to another city. 0 takes
// DefaultTravelMinDistanceKm.
MinDistanceKm float64
// Action is what an impossible journey does: RateBlock (the default, 403
// auth.impossible_travel), RateLog or RateChallenge.
Action RateAction
}
ImpossibleTravelRule configures impossible-travel detection: a sign-in whose location is too far from the user's previous sign-in for the time between them.
type Initializer ¶
Initializer receives the core handle once, during New.
type Joiner ¶
Joiner is a Scannable that also needs joined tables, for plugin data that lives in a table of its own. Optional.
type KeyRing ¶
type KeyRing struct {
// contains filtered or unexported fields
}
KeyRing holds the current sealing key and the previous ones, so a key can be rotated without losing what the old key sealed: new values are sealed with the current key, and a value is opened with whichever key opens it. Stateless session tokens, snapshot cache values and any plugin value sealed through Seal and Open can all ride on one ring.
A sealed value carries no key id: GCM refuses a wrong key in constant time, so trying each key in turn costs microseconds and keeps the sealed format identical to a single key's, which is what lets a deployment move from one key to a ring with nothing resealed. The order matters only as a hint: the current key first, then the previous ones newest first, so most values open on the first try.
func NewKeyRing ¶
NewKeyRing returns a ring whose first key seals and whose every key opens. Each key is SealKeySize bytes; a ring with no key, or with a key of another size, is an error. Rotation is: generate a new key, pass it first and the old one second, deploy; once every value sealed with the old key has expired or been resealed, drop it.
type KindOption ¶
type KindOption func(*IdentifierKind)
KindOption adjusts an IdentifierKind. Identifier plugins wrap these in their own option types, so every plugin offers the same words: Login, Recover, Contact.
func Contact ¶
func Contact() KindOption
Contact makes the kind contact information only: not unique, no sign-in, no recovery. It is a named option because "several users may share this" is a real product shape and should read as a choice, not an omission.
func Recover ¶
func Recover() KindOption
Recover allows the kind to receive recovery messages, which implies Unique.
func ScopedByTenant ¶
func ScopedByTenant() KindOption
ScopedByTenant makes uniqueness per tenant: the same value may belong to one account in each tenant, and every create and lookup of the kind then needs a tenant in ctx (WithScope), or fails with ErrNoScope.
func Unique ¶
func Unique() KindOption
Unique requests a unique index without making the kind a sign-in method.
type MaintainOptions ¶
type MaintainOptions struct {
// DryRun reports what the run would do, in the same counters, without
// sending, writing or deleting anything.
DryRun bool
}
MaintainOptions are what one Maintain run is asked to do.
type Maintainer ¶
type Maintainer interface {
Maintain(ctx context.Context, o MaintainOptions) (map[string]int64, error)
}
Maintainer is a plugin with a job for Maintain, such as plugin/stale. Maintain returns counters named by what they count ("warned", "deleted"); Auth.Maintain prefixes each with the plugin's name. It must be idempotent: a scheduler may run it twice, and a second run right after the first finds nothing left to do. A run that cannot finish returns the counters it has with an error.
type MemorySecuritySink ¶
type MemorySecuritySink struct {
// contains filtered or unexported fields
}
MemorySecuritySink keeps events in memory, for tests and examples: register it with WithSecuritySink, call Auth.FlushSecurity, read Events. Safe for concurrent use.
func (*MemorySecuritySink) Events ¶
func (m *MemorySecuritySink) Events() []SecurityEvent
Events returns a copy of everything received so far, oldest first.
func (*MemorySecuritySink) OfKind ¶
func (m *MemorySecuritySink) OfKind(k SecurityKind) []SecurityEvent
OfKind returns the received events of one kind.
func (*MemorySecuritySink) Reset ¶
func (m *MemorySecuritySink) Reset()
Reset forgets what was received.
func (*MemorySecuritySink) WriteSecurity ¶
func (m *MemorySecuritySink) WriteSecurity(_ context.Context, batch []SecurityEvent) error
WriteSecurity appends a copy of the batch.
type Message ¶
type Message struct {
// Purpose says what the message is for; the delivering plugin picks its
// template by it.
Purpose Purpose
// Link is a URL the recipient opens, such as a verify or reset link.
// Empty for messages that carry only a code or only information.
Link string
// Code is a short code to type in, for channels such as SMS. Optional.
Code string
// Locale is the recipient's language as a BCP 47 tag, or "" for the
// default. Deliver fills it from the request when the sender left it
// empty; templates pick their language by it.
Locale string
// DefaultSubject and DefaultText are the sending plugin's own English
// wording for a Purpose of its own, which the delivering plugin's default
// templates cannot know: the delivering plugin cannot import the sender
// (organization imports email, not the reverse). A delivering plugin uses
// them only in its fallback case, so an app's own templates still win.
// With only DefaultText set, the subject stays the delivering plugin's
// generic one ("A message about your account" for email); an SMS uses
// DefaultText alone. Empty for the core's purposes, which every
// delivering plugin words itself.
DefaultSubject string
DefaultText string
// Metadata is free per-message data the sending operation attaches (an
// invitation id, a campaign) for the app's own sender to read; the core
// never reads it, and the delivering plugin passes it through. The
// sending plugin bounds its size.
Metadata map[string]string
// Data is a typed payload whose type the Purpose's sending plugin
// documents (organization.InvitationData for an invitation), for an
// app template to read with a type assertion: better-auth's type-safe
// template variables. The core never reads it and never logs it; the
// delivering plugin passes it through. It is `any` because each
// Purpose has its own type and the core cannot name the sender's; the
// sender's MessageDataDecoder restores that type when the message goes
// through WithDeliveryOutbox, and a message whose Data no registered
// plugin decodes is refused there rather than delivered without it.
Data any `json:"-"`
}
Message is something the core or a plugin asks an identifier plugin to send to one of its identities.
type MessageCatalog ¶
MessageCatalog translates an error message for a locale. Message returns the text for code in locale, or false when the catalogue has none, in which case the error's declared (English) message is used, so a half-translated catalogue never produces an empty message.
Why a catalogue and not translated errors: an Error is declared once at package level and its code is the contract; the message is presentation, chosen per request. A plugin never knows about languages, and an app can translate a plugin's codes without the plugin's help.
type MessageDataDecoder ¶
type MessageDataDecoder interface {
DecodeMessageData(p Purpose, raw json.RawMessage) (any, error)
}
MessageDataDecoder is a plugin that sends messages with Message.Data and can restore that Data's type from JSON, which WithDeliveryOutbox needs: a queued message is stored as JSON, and without its decoder the Data would come back as a map the template's type assertion rejects. It answers (nil, nil) for a Purpose that is not its own.
type Messages ¶
Messages is a MessageCatalog held in memory: locale tag, then error code, then the message. The lookup is the one PickLocale applies to templates: the exact tag, then its base language ("de-CH" falls back to "de"), both compared without regard to case, so a catalogue with "de" serves every German-speaking region, and a "de-CH" entry overrides it for one code only.
Its JSON form is {"de": {"auth.signed_out": "..."}}, so a translation file unmarshals straight into it; docs/reference/error-codes.json lists every code with its English message as the template to translate.
type Migration ¶
type Migration struct {
// Owner is CorePluginName or the name of the plugin that ships it;
// Migrations refuses a plugin migration that names another owner.
Owner string
// Name orders nothing; it only identifies. Prefix it with a number, as
// "0001_tables", so a listing reads in the order it was written.
Name string
// Statements run in order, one ExecContext each, written for the engine
// the migration was generated for.
Statements []string
}
Migration is one named change owned by the core or a plugin.
Name is its identity for ever: the ledger remembers it by Owner and Name, and a Statement changed after it ran stops the next Migrate with ErrChecksumMismatch instead of being silently skipped or run again.
Why the core has its own ledger rather than sqlb's contrib/migrate: when this was written that module required a sqlb tag that did not exist yet, and the core module still depends on the standard library and sqlb only, so importing contrib/migrate would add a requirement every consumer inherits. This ledger is deliberately small.
type MigrationState ¶
type MigrationState struct {
Owner string
Name string
// Applied is true when the ledger has a row for Owner and Name.
Applied bool
// Changed is true when the ledger has the migration with another
// checksum: the next Migrate stops with ErrChecksumMismatch.
Changed bool
}
MigrationState is one migration and whether the ledger has it.
type Migrator ¶
Migrator is a plugin that ships migrations of its own tables. The statements must be written for the dialect it is given; a plugin that supports an engine it cannot write for returns ErrEngineNotSupported.
The core calls Migrations only on an initialised plugin (after Init), and the plugin names every table through the Auth it was given there (a.TableName), never by concatenating a TableName constant, because the app may have renamed the table (RenameTable, WithTablePrefix) and the constant is only its label. A migration creates no foreign key and no cascade (ADR-0002): a table whose rows belong to a user is listed in UserTables instead, and DeleteUser removes the rows.
type MultiSessionConfig ¶
type MultiSessionConfig struct {
// Enabled is true when WithMultiSession was given.
Enabled bool
// Max is the most sessions one device may hold, the active one included.
Max int
}
MultiSessionConfig is the WithMultiSession setting as Config reports it.
type NewIdentity ¶
type NewIdentity struct {
UserID string
Kind IdentifierKindName
Raw string
// Verified creates the identity already verified, for a value a trusted
// source vouched for, such as a provider that reports email_verified.
Verified bool
// Primary makes it the primary identity of its kind. Requires Verified,
// or it is ignored until the identity is verified.
Primary bool
// ScopeID confines uniqueness to a tenant, for kinds ScopedByTenant.
ScopeID *string
}
NewIdentity is the input of CreateIdentity.
type NewUser ¶
type NewUser struct {
Name string
Image string
// ID, when set, is the user's id instead of a generated one: an import
// keeping another system's ids, or a plugin that must know the id before
// the row exists. Opaque text; a taken id is ErrUserExists.
ID string
// CreatedAt, when set, is the user's creation time instead of now, for
// an import keeping the original sign-up time.
CreatedAt time.Time
}
NewUser is the input of CreateUser.
type Observer ¶
type Observer interface {
Operation(ctx context.Context, e OperationEvent)
}
Observer sees every operation end. It is the hook for metrics, tracing and error reporting: one Observer, not an error callback plus a timing callback plus a log hook, so a trace span and an error counter are built from the same event and cannot disagree about what happened.
Operation runs on the request's goroutine after the result is known and before it is returned, so it must be quick and must not fail: it returns nothing, and a panic in it is the app's.
type ObserverFunc ¶
type ObserverFunc func(ctx context.Context, e OperationEvent)
ObserverFunc adapts a function to Observer.
func (ObserverFunc) Operation ¶
func (f ObserverFunc) Operation(ctx context.Context, e OperationEvent)
Operation calls f.
type Op ¶
type Op[In, Out any] struct { // contains filtered or unexported fields }
Op is an operation with a typed input and output.
A plugin keeps the value returned by Define and exposes it two ways: in its Operations list for the transports, and through a one-line typed method for Go callers. Both paths run the same pipeline.
func Define ¶
func Define[In, Out any](info OpInfo, run func(ctx context.Context, c *Call, in In) (Out, error)) *Op[In, Out]
Define declares an operation. The run function holds the feature's logic and is the only place that logic exists.
func (*Op[In, Out]) Call ¶
Call runs the operation from Go code: a service, a CLI, a GraphQL resolver. When ctx carries a request, the call uses that request's session and client and its effects reach that request's response.
func (*Op[In, Out]) Info ¶
Info returns the description the operation was defined with: its id, kind, requirement and path.
func (*Op[In, Out]) Invoke ¶
Invoke runs the operation for a transport. The input is decoded inside the pipeline, after the origin and requirement checks, so a cross-site request is refused before its body is even read.
func (*Op[In, Out]) OutputType ¶
OutputType returns the type of Out.
type OpInfo ¶
type OpInfo struct {
ID EndpointID
Kind OpKind
Require Requirement
// Path is relative to the plugin, such as "/sign-in". The core serves it at
// "/<plugin>/sign-in"; a core operation's path has no plugin segment.
Path string
// CrossSiteForm marks a Mutation that another site posts to as a plain
// HTML form: an OAuth provider's response_mode=form_post answer, which
// arrives without a trusted Origin and without the flow cookie (SameSite
// Lax keeps it off a cross-site POST). Such an operation only relays its
// fields into a redirect, so the pipeline skips the origin check for it
// and resolves no session: its Credential is CredentialNone whatever the
// request carried, and nothing a stray cookie held is visible to it. It
// must be Public and a Mutation; auth.New refuses anything else
// (ErrBadOperation), because a credential-bearing or session-reading
// operation without the origin check would be the login CSRF the check
// exists to refuse. authhttp decodes a form body for it without
// AcceptForms, and documents the form media type. The relay's own
// protection is the OAuth state it carries and the flow cookie the GET
// callback it redirects to still checks; the flag removes nothing from
// that step.
//
//ds:def id=core-opinfo-crosssiteform-7qn3kx8m owner=@khanakia stability=stable
CrossSiteForm bool
// SelfServed marks an operation whose HTTP wire format a standard fixes,
// such as an OAuth 2.1 token endpoint (RFC 6749: form-encoded body,
// HTTP Basic client authentication, {"error": "invalid_grant"} answers).
// The plugin serves it over HTTP from its own handler, which calls the
// typed Op.Call; Auth.Operations leaves it out, so no transport mounts a
// second, JSON-shaped copy of it. It is still wired: Auth.Operation finds
// it, hooks and WithDisabledOperations may name it, and every call runs
// the whole pipeline (rate limits, origin check, requirement, hooks), so
// the plugin's handler cannot skip a step. It also marks an operation
// with no wire format at all, one only the plugin's own handler or Go
// API calls (scim.request, which admits each SCIM request; handoff's
// generate under ServerOnly): leaving it out of Auth.Operations is what
// keeps authhttp.Handler, the adapters, the OpenAPI document and the
// generated TypeScript client from offering a route that only answers
// auth.ErrUnknownOperation.
//
//ds:def id=core-opinfo-selfserved-m4q8xk2w owner=@khanakia stability=stable
SelfServed bool
// SignIn marks an operation whose every successful call signs the
// caller in: it authenticates someone who was not signed in and issues
// their session (a password or code sign-in, a provider callback, a
// second factor that promotes a pending session, a device-code token).
// It is metadata for observers, so an audit log can type a third-party
// sign-in method's operations as sign-ins without a list of ids; the
// pipeline reads it for one thing only, WithGeoRules, which judges the
// operations marked here and no others. An operation that signs in only sometimes
// (a poll that is usually still pending) or that creates an account
// (a sign-up, which observers record as its own event) leaves it false.
//
//ds:def id=core-opinfo-signin-5vt8kq2m owner=@khanakia stability=stable
SignIn bool
}
OpInfo describes an operation to the pipeline and to every transport.
func OperationOf ¶
OperationOf returns the operation whose pipeline is running in ctx: what a data hook, which has no Call, needs to know how a row came to be written, such as which sign-in method created a session. False outside an operation (a direct Go call to the core API, a job). It is set before the hooks and the operation run, inside the transaction.
type OpKind ¶
type OpKind string
OpKind says whether an operation reads or changes state. Transports map it: HTTP serves a Query as GET and a Mutation as POST; GraphQL exposes them as a query and a mutation. The origin check applies to mutations only.
type Operation ¶
type Operation interface {
Info() OpInfo
// Plugin is the name of the plugin that registered the operation, or
// CorePluginName for the core's own.
Plugin() string
// Route is the full path, "/<plugin><Path>", set when auth.New wires it.
Route() string
// Invoke runs the operation for a transport. decode fills the typed input
// from the transport's request; ctx must come from Auth.WithRequest.
//
// The input and output are any because one transport serves every
// operation through this one interface, and the concrete types differ per
// operation. This is the boundary where typed Go meets an encoded request;
// decode checks the input's shape, and the output goes straight to an
// encoder. Go callers use the typed Op.Call instead and never see any.
Invoke(ctx context.Context, decode func(dst any) error) (any, error)
// InputType and OutputType are the Go types of In and Out, for a
// documentation generator that writes schemas from their fields and
// json tags; nothing else can see them through this interface.
InputType() reflect.Type
OutputType() reflect.Type
// contains filtered or unexported methods
}
Operation is one feature, as every transport sees it.
The interface is sealed by an unexported method, so the only way to make an Operation is Define. That is the point: an operation that could be built by hand could skip the pipeline, and the pipeline is where the rate limits, the origin check, the session requirement and the hooks live.
type OperationEvent ¶
type OperationEvent struct {
Op OpInfo
Plugin string
Transport Transport
// Duration is from the pipeline's first check to the result, measured on
// the configured clock.
Duration time.Duration
Err error
}
OperationEvent describes one finished call, successful or not, to an Observer. Err is nil for success and otherwise the error the caller got, so an observer can count refusals by code (errors.As to *Error) without seeing any input or output: those stay with the operation, because a metrics sink is the last place a password or a token should travel to.
type OperationHook ¶
type OperationHook struct {
Match []EndpointID
Before func(ctx context.Context, c *Call) error
After func(ctx context.Context, c *Call, result any) error
// Finally runs once per call after the call's transaction has ended,
// commit or rollback, before the observer, on every outcome, with the
// error the caller gets (nil on success). It returns nothing and cannot
// change the answer: it is for recording what happened (an audit row
// for a failed sign-in, a counter), never for deciding. It runs for a
// refusal before the input was decoded too (rate limit, origin,
// requirement), with c.Input nil then.
Finally func(ctx context.Context, c *Call, err error)
}
OperationHook runs before and after chosen operations, on every transport.
After receives the result as any because one hook can match operations with different output types; a hook that needs the concrete type asserts it for the operations it named in Match.
Match lists the operations by id; an empty Match means every operation. A Before error stops the call and becomes its error. An After error also fails the call, and the call's effects are then dropped, so a hook can veto a sign-in after the fact without a cookie leaking out.
type OperationHooker ¶
type OperationHooker interface {
OperationHooks() []OperationHook
}
OperationHooker contributes hooks that run before and after chosen operations, on every transport.
type OperationProvider ¶
type OperationProvider interface {
Operations() []Operation
}
OperationProvider contributes operations. Every transport exposes them, and every call to them goes through the pipeline.
type Option ¶
type Option func(*settings)
Option configures New.
Functional options rather than an exported Config literal because New's validation depends on the combination of settings; a literal invites a caller to skip New and run an unvalidated instance.
func EnvOptions ¶
EnvOptions builds options from environment variables through lookup — pass os.LookupEnv. The lookup is a parameter, not a hidden read of the process environment, so a test passes a map and nothing depends on the machine it runs on. An unset variable leaves the default; a malformed one is an error naming it, so a typo fails the deploy instead of silently running with the default.
func Options ¶
Options groups several options into one, so a shared base list can be passed where a single Option is expected and a test harness can wrap a setting with the ones every instance needs. Options run in order.
func RenameTable ¶
RenameTable gives one table a real name of the app's choosing. t must be a declared table value — auth.TableUser, auth.TableIdentity, apikey.TableKey, any plugin's exported Table… — never a hand-built sqlb.Table: New refuses a table that the core or a registered plugin does not declare, so a rename cannot miss its target. The name is used exactly as given; WithTablePrefix does not apply to it. Decide before the first deploy: renaming a table that exists is an ALTER TABLE the app runs itself, since the migration ledger records what was created and not what it was called.
func WithAccountDeletion ¶
func WithAccountDeletion(d AccountDeletion) Option
WithAccountDeletion turns self-service deletion on and configures it; see AccountDeletion. Without it the two routes do not exist and DeleteAccount answers ErrAccountDeletionOff; auth.WithAccountDeletion(auth.AccountDeletion{}) is the plain form, deleting at once.
func WithAllowedHosts ¶
WithAllowedHosts names the hosts the deployment serves besides the base URL's: exact ("app.example.com", "app.example.com:8443") or a wildcard for every subdomain of a domain ("*.example.com", the same rule as WithTrustedOrigins patterns). A request whose Request.Host is allowed gets that host in BaseURL(ctx), under the base URL's scheme, so the links a plugin sends point at the host the user is on; and each allowed host is a trusted origin for cookie-authenticated mutations, so an app does not list its hosts twice. A request on a host that is not allowed, or with no host, falls back to the static base URL. The transport fills Request.Host from the request, behind its trusted-proxy rule when there is one; a host the client made up is never allowed by this list, which is the whole point of having one.
func WithBackground ¶
func WithBackground(run BackgroundFunc) Option
WithBackground replaces how post-commit work such as message delivery leaves the request. The default starts a goroutine. A platform that ends the process with the response (a serverless runtime with a waitUntil, a worker pool with a queue) passes its own runner. The core still bounds each task (DeliveryTimeout) and WaitDeliveries still waits for them, because the runner is told when a task ends through the function it is handed.
func WithBotCheck ¶
func WithBotCheck(c BotChecker, p RiskPolicy) Option
WithBotCheck turns on the bot check for signed-out mutations and sign-in operations. The policy's zero value blocks and fails open.
func WithCodeLength ¶
WithCodeLength overrides DefaultCodeLength, the number of digits in every one-time code (SMS, email, sign-in by code), between MinCodeLength and MaxCodeLength. Every digit multiplies the values a guesser must cover by ten; the attempt limit and the rate limits stay as they are, so a longer code is strictly harder to guess and a shorter one is accepted only down to four digits, where five attempts still give one chance in two thousand.
func WithDB ¶
WithDB sets the database and its sqlb dialect. The library uses this handle for every statement; the app may use the same one, and Auth.Tx lets both write in one transaction.
func WithDBAuto ¶
WithDBAuto is WithDB with the dialect found by DetectDialect, for an app that would rather not name it. New runs the detection, bounded by DetectTimeout, and fails with ErrUnknownDialect when the database is none of the three. WithDB stays the explicit form, and the one to use when the driver is wrapped and New should run no query.
func WithDBRateStore ¶
func WithDBRateStore() Option
WithDBRateStore counts rate limits in the auth database, in the table auth_rate_limit, which Migrate creates. It is the store to pick when an app runs more than one instance and has no shared cache: every instance sees the same counters.
func WithDataHooks ¶
WithDataHooks registers data hooks from the app itself, without writing a plugin: a BeforeDelete on User removes the app's own rows that reference the user inside the deleting transaction, or refuses the deletion with an error (an open subscription, an owner of a shared workspace). They run after every plugin's hooks, in the order given.
func WithDefaultLocale ¶
WithDefaultLocale sets the locale of a message whose request carried no language (no Accept-Language, or a Go call), as a BCP 47 tag such as "de". A plugin's templates by locale (email.TemplatesByLocale and the like) are looked up with it through PickLocale. Without it such a message has no locale and gets the plugin's default template.
func WithDefaultPageSize ¶
WithDefaultPageSize sets how many rows a list returns when the caller asks for no particular number, across every list that reads it (PageSize), as better-auth's advanced.database.defaultFindManyLimit caps findMany. It changes only the default: each list keeps its own maximum, so the option can make a list answer fewer rows by default but never more than the list allows. New refuses a negative value; 0 keeps each list's own default.
func WithDeferredSessionRefresh ¶
func WithDeferredSessionRefresh() Option
WithDeferredSessionRefresh makes every read leave the session's expiry alone: a query (GET /session, a middleware lookup, any Query operation) only reports NeedsRefresh in the session result when the slide is due, and the client asks for it with POST /session/refresh (RefreshSession), or by any mutation, which still slides as before. For a deployment that serves reads from a replica, or that wants GET to be a pure read. Without the option every lookup slides when due, as before.
func WithDeliveryOutbox ¶
WithDeliveryOutbox makes delivery survive a crash between the commit and the send. Off by default: Deliver then sends after the commit in the background, and a message whose process dies first is lost (the user can ask again).
With it, Deliver also writes the message to auth_delivery_outbox in the transaction that produced it, so the row exists exactly when the data the message refers to does. The post-commit background send runs as before and deletes the row once the deliverer accepted it; a failed or interrupted send leaves the row, and SendOutbox (called by "auth sweep", or by the app's own worker on a schedule) sends it again with a doubling backoff, up to MaxDeliveryAttempts. Delivery is at least once: a process that dies after the send and before the delete sends the message again.
The payload is sealed (AES-256-GCM) under the first key, or under a key derived from WithSecret when none is given; more than one key is a rotation, as with NewSnapshotCache. New refuses the option without a database or a key. Migrate creates the table.
func WithDeviceRecognizer ¶
func WithDeviceRecognizer(r DeviceRecognizer) Option
WithDeviceRecognizer replaces the rule WithUnknownDeviceNotification judges a device by. It does not turn the notification on: New refuses it without WithUnknownDeviceNotification, since it would change nothing.
func WithDeviceSessionExtras ¶
func WithDeviceSessionExtras() Option
WithDeviceSessionExtras runs the WithSessionExtras function for every session of the device list too (GET /device-sessions, Auth.DeviceSessions), as better-auth's shouldMutateListDeviceSessionsEndpoint does, so an account switcher shows each account's plan or avatar without a request per account. For each listed session the function gets a copy of the call whose Session and User are that session's, and whose Effects are discarded: listing must not issue a cookie or a redirect on another account's behalf. One call per listed session, at most MultiSessionConfig.Max. New refuses it without WithMultiSession and WithSessionExtras.
func WithDialect ¶
WithDialect sets only the dialect, for building queries without a database.
func WithDisabledOperations ¶
func WithDisabledOperations(ids ...EndpointID) Option
WithDisabledOperations switches operations off by id, such as password.EndpointSignUp for an app that creates its accounts itself. A disabled operation has no route on any transport (the request is answered like an unknown path) and its Go method returns ErrUnknownOperation, so no entry point serves it by accident. New refuses an id nothing registered.
It disables whole operations only. A rate rule a plugin declared for an operation cannot be removed, because a limit that can be switched off by configuration is one a deployment switches off by mistake.
func WithErrorURL ¶
WithErrorURL is the page a failed browser flow (an OAuth callback, a magic link) lands on when the flow names none, with the error code in the query ("?error=oauth.denied"), as better-auth's onAPIError.errorURL is. It must pass TrustsReturnURL (a same-site path, or a URL on a trusted origin), or New refuses it, so no failure can send the browser off-site. authhttp.ErrorPage serves a page for it to point at. Without it a flow with no error page of its own answers its failure as JSON.
func WithFetchMetadata ¶
func WithFetchMetadata() Option
WithFetchMetadata refuses a mutation whose request says Sec-Fetch-Site: cross-site and whose Origin is not trusted, whether or not it carries a cookie. The origin check alone protects cookie-authenticated calls; a sign-in has no cookie yet, so a form on another site could sign the victim into the attacker's account (login CSRF) and the origin check would not look. Browsers send the header on every request and no script can change it, so the check costs nothing and cannot be forged from a page. It is opt-in because a client that sends no header (an old browser, a native app, curl) is unaffected either way, and an app deciding to rely on it should know it did.
func WithFreshWindow ¶
WithFreshWindow overrides DefaultFreshWindow, how recent a sign-in a FreshSignIn operation needs. Shorter is safer and asks users to sign in again more often.
func WithGeoLocator ¶
func WithGeoLocator(l GeoLocator) Option
WithGeoLocator sets where client addresses are looked up. It turns nothing on by itself: New refuses it without WithGeoRules or WithImpossibleTravel, and refuses either of those without it.
func WithGeoRules ¶
WithGeoRules applies country allow and deny rules to every sign-in operation (OpInfo.SignIn), before the credentials are checked. It needs WithGeoLocator. A Go call to CreateSession is the app's own code and is not judged: there is no client address.
func WithIDs ¶
WithIDs replaces the id generator. prefix is one of the IDPrefix constants or a plugin's own; the generator must return a unique, opaque string.
func WithIPReputation ¶
func WithIPReputation(c IPReputation, p RiskPolicy) Option
WithIPReputation turns on the IP check for signed-out mutations and sign-in operations. The policy's zero value blocks and fails open.
func WithIPv6Prefix ¶
WithIPv6Prefix makes ByIP rules count an IPv6 address by its first bits, typically 64, so one subscriber's allocation is one counter. IPv4 addresses are unaffected. The default counts whole addresses; New refuses a value outside 1..IPv6PrefixBits.
Addresses are canonicalized before counting in every case ("::1" and "0:0:0:0:0:0:0:1" are one counter), and a value that is not an address is counted as given.
func WithImpossibleTravel ¶
func WithImpossibleTravel(r ImpossibleTravelRule) Option
WithImpossibleTravel compares each new sign-in's location with the user's previous one and applies the rule's action when no journey could connect them: distance / elapsed time above the speed limit, for a distance above the minimum. It needs WithGeoLocator and sessions that record addresses (not WithoutIPTracking), and the default history needs a session list (not WithStatelessSessions) unless WithTravelHistory supplies one. A sign-in with no previous one, or whose locations lack coordinates, is not judged.
func WithLogger ¶
WithLogger sets where the core logs internal errors and warnings.
func WithLoopbackReturn ¶
func WithLoopbackReturn() Option
WithLoopbackReturn lets a flow send the browser back to http://127.0.0.1 or http://[::1] on ANY port, with a path: the return address of a native app that signs in through the system browser and listens on an ephemeral loopback port (RFC 8252 section 7.3, which says the server must accept any port there because the app cannot know it in advance). It affects TrustsReturnURL only. It never makes a loopback origin trusted for cookie-authenticated mutations: a page served from any local port would then be able to make credentialed requests, which is the CSRF the origin check exists to refuse, so TrustsOrigin ignores it. Only the two literal addresses count, never "localhost": the RFC says not to rely on name resolution, and a hosts file can point the name anywhere. Scheme http only: TLS on loopback has no certificate to pin. A desktop flow does put a credential in that URL (examples/desktop-sign-in sends the hand-off token in the query, as RFC 8252 sends the authorization code): it is single-use, expires within minutes, goes only to a listener on this machine, and is exchanged for a session over bearer. It is not yet bound to the app instance that started the flow (no PKCE-style verifier), which is the remaining gap for custom-scheme returns, where another app can register the same scheme.
func WithMessages ¶
func WithMessages(c MessageCatalog) Option
WithMessages sets the catalogue every transport renders error messages from, in the request's locale (Client.Locale, then WithDefaultLocale). Without one, or for a locale or code the catalogue lacks, the message is the declared English one; the code and the status never change, so a client matching codes sees no difference. New refuses a Messages map whose locale keys are not language tags, since a typo there would hide a whole language silently.
func WithMultiSession ¶
WithMultiSession lets one device hold several signed-in accounts and switch between them, as a browser does between a work and a personal account. max is the most sessions a device may hold, the active one included; 0 means DefaultMaxDeviceSessions, less than 0 is refused by New. A sign-in that would exceed it is refused with ErrTooManyDeviceSessions and creates nothing, so the user signs one account out first.
It adds three operations, GET /device-sessions, POST /device-sessions/switch and POST /device-sessions/revoke, and changes sign-out: with several accounts on the device, POST /sign-out ends every one of them, because "sign out" on a shared computer must leave nothing behind; one account is ended with /device-sessions/revoke. Without the option nothing changes.
func WithObserver ¶
WithObserver sets the observer every call reports to. Without one nothing is measured, and the pipeline reads no clock for it.
func WithOperationHooks ¶
func WithOperationHooks(h ...OperationHook) Option
WithOperationHooks registers operation hooks from the app itself: a Before hook on EndpointDeleteAccount refuses self-service deletion for some users while an admin can still delete them, an After hook on a sign-in records it. The same rules as a plugin's hooks apply: a Match that names an operation nothing registered fails New, and an error from a hook fails the call and drops its effects.
func WithPendingSessionTTL ¶
WithPendingSessionTTL overrides PendingSessionTTL, how long a user has to pass the second factor after the first. Shorter leaves a half-finished sign-in lying around for less time; longer suits a factor that takes a while, such as a code sent by post-like SMS. New refuses zero or less.
func WithPlugins ¶
WithPlugins registers plugins. Calls accumulate, so a shared base list and a per-app extra list compose.
func WithProfileFields ¶
WithProfileFields lets POST /auth/profile (Auth.UpdateProfile) set the listed app columns of the signed-in user's own row, ProfileRequest.Fields: better-auth's updateUser with additionalFields. The rules are the sign-up fields': each must be a WithUserColumns column (New refuses a core column, a plugin's or an undeclared name), a value must have the column's type, and anything else is ErrFieldInvalid with nothing written. A column the user must not change for themselves (a plan, a role) is simply not listed; the app writes it with SetUserColumn from its own route.
func WithRateChallenger ¶
func WithRateChallenger(ch RateChallenger) Option
WithRateChallenger sets what passes a RateChallenge rule's challenge, for an app whose proof is not a registered plugin's (a proof of work, its own captcha service). Without it the one registered plugin that implements RateChallenger (plugin/captcha or plugin/pow) is used. New refuses two challengers (the option and such a plugin, or two such plugins), since which one a client must satisfy would be ambiguous, and refuses a RateChallenge rule with no challenger at all, since nothing could ever pass it.
func WithRateExempt ¶
func WithRateExempt(fn RateExemptFunc) Option
WithRateExempt exempts the calls fn accepts from ByIP rules. ByIdentifier rules are never exempt: they count guesses at one account whatever address they come from, and an exemption there would hand an attacker behind the exempt address unlimited attempts at every user.
func WithRateLimit ¶
WithRateLimit puts one blanket ByIP limit on every operation, counted per operation, on top of the plugins' own rules: at most limit calls per window from one address to each route. It is the floor under a flood of requests to an operation nobody thought to limit; the plugins' rules stay the ceiling on the sensitive ones. Off by default, because the right number depends on what sits in front of the app.
func WithRateRules ¶
WithRateRules adds the app's own rules. A rule for an operation a plugin already limits REPLACES that plugin's rules for the operation, so an app that finds a shipped budget too tight (or too loose) states the one it wants; a rule for an operation with none adds the first. What an app cannot do is remove a limit: every rule needs Limit of one or more, and WithDisabledOperations is the way to switch an operation off.
Rules are checked like a plugin's: the operation must exist and the rule must be valid, or New refuses.
func WithRateStore ¶
WithRateStore sets where rate-limit counters live. The default counts in memory, which is correct for one process only.
func WithSQLTypes ¶
WithSQLTypes overrides column types for engine, for every table the core writes and every plugin that implements TypedMigrator: UUID ids on Postgres (SQLTypes{ID: "UUID"}, with WithIDs generating UUIDs), a wider Long on MySQL, a dialect-compatible spelling for an engine that speaks one of the three (CockroachDB as Postgres, MariaDB as MySQL, libSQL as SQLite). Only the fields set in t change; an empty field keeps the built-in spelling. Called again for the same engine, the later call wins field by field.
It cannot add a fourth engine: the core's upserts, unique-violation detection and index statements are written per engine, and the sqlb dialect renders only Postgres, MySQL and SQLite, so New refuses any other engine name with ErrEngineNotSupported. The override must still store what the core writes: an id column typed UUID needs WithIDs to generate UUIDs.
func WithSealer ¶
WithSealer makes s the Sealer every plugin that keeps secrets at rest uses when it was given no key of its own: an explicit plugin key still wins, and without WithSealer those plugins derive a KeyRing from WithSecret as before. A nil s is refused by New. Session tokens and the snapshot cache are not sealed through it: they are opened on every request, where a network round trip to a key service would be the wrong trade.
func WithSecondaryStorage ¶
func WithSecondaryStorage(kv SecondaryStorage) Option
WithSecondaryStorage keeps sessions (NewStorageSessionStore) and rate-limit counters (NewStorageRateStore) in kv instead of the database, as better-auth's secondaryStorage option does. It is WithSessionStore and WithRateStore in one; a later WithSessionStore or WithRateStore replaces its half. Users, identities and verification tokens stay in SQL.
func WithSecret ¶
WithSecret sets one secret from which every sealing key the deployment did not pass explicitly is derived: the stateless session key, the snapshot and compact cache keys, and a plugin's key when it asks Config.DeriveKey. Each is HKDF-SHA256 of the secret under its own fixed label, so the features share one variable in the secret store and no key material. An explicit key still wins where one was given.
Rotation: pass the new secret first and the old one after it. Keys derived from the first seal; keys derived from every secret open (KeyRing), so a stateless token or a sealed plugin value from before the rotation still opens, and the old secret is dropped once everything sealed under it has expired or been resealed. That is the same "current first, previous after" shape every ring-taking option has.
There is no default and no development fallback. A missing or short secret fails New (ErrSecretTooShort), because a generated one would sign every user out at the next restart and nobody would know why.
func WithSecurityLog ¶
func WithSecurityLog(q SecurityQueue) Option
WithSecurityLog registers the logger sink: every event is one structured line on the Auth's logger (WithLogger), at Info, Warn or Error by severity (info, warn, alert). It is the zero-setup way to see the stream.
func WithSecuritySink ¶
func WithSecuritySink(sink SecuritySink, q SecurityQueue) Option
WithSecuritySink registers a sink for the security event stream; call it once per sink. Each sink has its own bounded queue (shaped by q, whose zero value is fine) and its own goroutine, started by New and stopped by Close, so a slow sink delays only itself.
Delivery never blocks a request. Emitting is a non-blocking send into the queue: when the queue is full the NEW event is dropped and counted (SecurityStats.Dropped), because blocking the sign-in that triggered it, or growing without bound under the very flood being reported, is worse than a gap in the feed. The sink's goroutine hands the queue to WriteSecurity in batches of at most q.BatchSize, or earlier once q.FlushEvery passes; the number of dropped events is logged once per interval.
Events are not durable: a process that dies loses what was queued, and Close drains the queues first (bounded). plugin/audit is the durable record.
func WithSession ¶
func WithSession(c SessionConfig) Option
WithSession overrides DefaultSessionConfig, the idle timeout, the hard lifetime and how often an active session's expiry is written. A zero field keeps DefaultSessionConfig's value, so WithSession(SessionConfig{IdleTimeout: time.Hour}) changes the idle timeout alone; New refuses a negative one.
func WithSessionArchive ¶
func WithSessionArchive() Option
WithSessionArchive keeps a revoked session's row, marked with the time it was ended (Session.RevokedAt), instead of deleting it: sign-out, "sign out everywhere", revoking by id and a password reset all leave an audit trail that RevokedSessions reads and Sweep removes after the retention. A revoked row is never found by a lookup, so nothing changes for the user; only the SQL store supports it, and New refuses the option with another store or with stateless sessions, which have no row to keep.
func WithSessionCache ¶
func WithSessionCache(c SessionCache, ttl time.Duration) Option
WithSessionCache puts c in front of the session lookup for ttl per entry: 0 means DefaultSessionCacheTTL, more than MaxSessionCacheTTL is clamped to it. Built-in caches: NewSnapshotCache (the client holds a sealed copy) and NewMemorySessionCache (this process); contrib/redis has a shared one.
func WithSessionExtras ¶
func WithSessionExtras(fn SessionExtras) Option
WithSessionExtras adds app data to the session response (GET /session and Auth.Session). The value is encoded as the "extras" field; a nil value or no function leaves the field out, so the response is unchanged for every app that does not use it.
func WithSessionPolicy ¶
func WithSessionPolicy(p SessionPolicy) Option
WithSessionPolicy sets the per-client lifetime policy. Without one every session gets SessionConfig. Pending second-factor sessions and impersonation sessions keep their fixed lifetimes whatever the policy says.
func WithSessionRevocations ¶
func WithSessionRevocations(r SessionRevocations) Option
WithSessionRevocations gives stateless sessions a revocation list: the sign-out of a stateless session, revoking it by id, ending another account on the device and the second-factor promotion add the session's id to r, and every stateless lookup consults it, so the token stops working at once instead of only losing its cookie. Built-in lists: NewMemorySessionRevocations (this process); contrib/redis has a shared one. The cost is one list lookup per request. New refuses it without WithStatelessSessions: a session in a store is ended by deleting it.
func WithSessionStore ¶
func WithSessionStore(s SessionStore) Option
WithSessionStore replaces the SQL session store. See SessionStore for what a replacement gives up (transactions) and NewMemorySessionStore for the one that ships in the core.
func WithSignUpFields ¶
WithSignUpFields lets a sign-up body set the listed app columns of the new account (Fields). Each must be a column WithUserColumns declared: New refuses a core column, a plugin's column, or a name nobody declared, because a column the app did not mark writable by strangers (a role, a ban) must not become one by a typo.
func WithStatelessRefreshWithoutRead ¶
WithStatelessRefreshWithoutRead lets a stateless request skip the user query: the token carries the user as the last full read found it, and for interval after that read (0 means DefaultStatelessReadInterval, more than MaxStatelessReadInterval is clamped to it) every request, and every sliding refresh, uses that copy. A token near expiry is then reissued without loading the user, as better-auth's refreshCache does; the first request after the interval loads the user again, applies every SessionChecker condition and the sessions_valid_after cutoff, and carries the fresh copy in a reissued token.
The trade-off, stated plainly: a ban, a "sign out everywhere" and a password reset reach a token only at its next full read, so a session they end keeps working for up to interval. What does not wait: an operation that requires a FreshSignIn always loads the user (a password change or an account deletion never runs on a copy), a session revoked through WithSessionRevocations is refused at once, and raising WithStatelessVersion or replacing the key ends every token at once. Without the option every request loads the user, as before. New refuses it without WithStatelessSessions.
func WithStatelessSessions ¶
WithStatelessSessions makes the session token carry the session itself, sealed with the first key (SealKeySize bytes each, AES-256-GCM), so a request needs no session store and no session row. Every request still loads the user (one primary-key query) with the SessionChecker conditions, so a ban applies at once; DeleteUserSessions ("sign out everywhere", a password reset) sets auth_user.sessions_valid_after, which refuses every token issued before it.
What stateless mode gives up, because the server holds nothing per session: a session cannot be listed beyond the request's own or revoked one by one (Sessions and DeleteSession answer ErrSessionsUnavailable), a sign-out on one device is only the cookie being cleared, and the token is longer than a stored session's (about 400 characters). Replacing the key is the global logout switch: every token sealed with the old key stops opening. To rotate WITHOUT signing everyone out, pass the new key first and the old one after it: new tokens are sealed with the first key and every key opens (KeyRing); drop the old key once its tokens have expired.
WithStatelessSessions and WithSessionStore exclude each other; New refuses both, and refuses no key at all.
func WithStatelessVersion ¶
WithStatelessVersion numbers the stateless session tokens. A token carries the version it was sealed with and opens only while it matches: raising the number signs every user out at once without rotating the key, and lowering it back does not revive them, because the tokens have been cleared by their browsers by then. The default is 0; a negative number is refused, and the option without WithStatelessSessions is refused too, since no token would carry it.
func WithTablePrefix ¶
WithTablePrefix prepends prefix to the real name of every table, the core's and every plugin's, except a table RenameTable names explicitly. It is the one-line way to keep two apps, or two instances of one app, in one database or schema: WithTablePrefix("billing_") puts the users in billing_auth_user. The prefix must be a plain identifier start (letters, digits, underscores); it is written verbatim, so end it with an underscore if you want one.
func WithTokenRotation ¶
func WithTokenRotation(r TokenRotation) Option
WithTokenRotation gives a session a new token every time its expiry slides (once per refresh interval, SessionConfig.RefreshAfter), and treats the replaced token, presented after the grace window, as stolen: the session is revoked at once (every device holding it is signed out) and a SecuritySessionTokenReuse event is emitted. Within the grace window the old token still works, so concurrent requests and tabs are not signed out.
It catches a copied token as soon as the real user and the thief have both used the session across a rotation: whichever of the two presents the replaced token later ends the session for both. A response that never reached the client (a dropped connection right after a rotation) looks the same and signs that client out once the grace passes; that is the price of the detection, the same as SuperTokens' refresh-token rotation.
The new token reaches the client like any refreshed session: a new cookie, or Set-Auth-Token in bearer mode, on whichever response did the rotation. Only the request pipeline rotates; ResolveSession and the multi-session device list slide the expiry without changing the token, because their callers have no way to hand a new token on.
It needs a store with the SessionTokenRotator upgrade (the SQL and memory stores and contrib/redis have it) and refuses stateless sessions, whose token is the session and has nothing to compare against. Off by default.
func WithTracer ¶
WithTracer sets the tracer every call reports its spans to. Without one no span is started and the pipeline does no extra work.
func WithTravelHistory ¶
func WithTravelHistory(h SignInHistory) Option
WithTravelHistory replaces where the previous sign-in is read from. New refuses it without WithImpossibleTravel.
func WithTrialAbuseCheck ¶
func WithTrialAbuseCheck(c TrialAbuseChecker, p RiskPolicy) Option
WithTrialAbuseCheck turns on the sign-up check, run when a user's first identifier is stored. An account made without an identifier (a pure provider link) is not judged. A block returns the error from CreateIdentity, so the sign-up that called it fails with the user it created rolled back when the operation runs in a transaction.
func WithTrustedOriginFunc ¶
func WithTrustedOriginFunc(fn OriginFunc) Option
WithTrustedOriginFunc adds a function to the origin check, asked only for an origin the static list and the patterns did not accept. It is for a set of origins that changes while the process runs: one per tenant read from a table, or a preview deployment per branch.
The function must be fast and must not fail open: an error of its own becomes "false", because the check runs on every cookie-authenticated mutation and a slow or lenient answer there is the CSRF hole.
func WithTrustedOrigins ¶
WithTrustedOrigins adds origins allowed to send cookie-authenticated mutations, such as an admin app on another subdomain.
func WithUnknownDeviceNotification ¶
func WithUnknownDeviceNotification() Option
WithUnknownDeviceNotification sends the user a message (PurposeUnknownDevice, to their best recovery channel, as a password reset goes) when they sign in from a device not seen before, judged by SessionDevices unless WithDeviceRecognizer replaces it. Every full session counts as a sign-in, including the one a second factor promotes; a pending session waits for its promotion, and an impersonation is not the user's sign-in.
It never blocks a sign-in: the check and the send run after the sign-in commits, in the background (WithBackground, waited for by WaitDeliveries), and a failure of either is logged. A user without a verified recovery identity gets nothing. Under WithDeliveryOutbox the message is queued and retried like any other.
func WithUserColumns ¶
func WithUserColumns(cols ...ColumnExtension) Option
WithUserColumns adds columns to auth_user that the app owns, with no plugin to write: a plan, a locale, a flag. Each column is declared, migrated and loaded exactly as a plugin's (ColumnExtension, LoadUser), and written through SetUserColumn with AppName as the owner, so the one rule "writes are closed" holds for the app's columns too. Table may be left empty and is auth_user. New refuses a column a plugin also adds.
func WithUserLocale ¶
WithUserLocale names a text column of auth_user (one WithUserColumns declared) holding the user's chosen locale, better-auth's i18n "session" detection: for a signed-in request it wins over the client's Accept-Language and locale cookie, so a user who picked German in settings gets German mails and errors on any device. It is read lazily, once per request and only when a message or Locale needs it, and only once the session is resolved: an error answered before that (the session lookup's own) is in the client's locale. An empty or malformed value is ignored.
func WithVerificationStore ¶
func WithVerificationStore(s VerificationStore) Option
WithVerificationStore keeps verification tokens in s instead of the auth_verification table. NewStorageVerificationStore puts them in a key-value store (contrib/redis NewVerificationStore is that over Redis); NewMemoryVerificationStore keeps them in this process. See VerificationStore for what a store outside the database gives up. The table is still created, so switching back needs no migration.
func WithoutCSRFCheck ¶
func WithoutCSRFCheck() Option
WithoutCSRFCheck turns off the origin check that refuses a cookie-authenticated mutation from an untrusted origin, and the Sec-Fetch-Site check of WithFetchMetadata with it: better-auth's advanced.disableCSRFCheck. Off by default; New logs WarnCSRFCheckOff at every start while it is on.
The attack it opens is cross-site request forgery: any page the user visits can POST to the auth routes and the browser attaches the session cookie, so another site can change the password (where no fresh sign-in is required), revoke sessions, unlink a provider, create an API key or sign the user out. Prefer the switches that keep the check: WithTrustedOrigins (patterns and custom schemes), WithTrustedOriginFunc, or bearer mode, which carries no cookie and is never origin-checked. Use this only behind another CSRF defence the app runs itself (a proxy that checks Origin, a synchronizer token), or in a test. authui's form pages keep their own origin check, which this does not reach.
func WithoutIPTracking ¶
func WithoutIPTracking() Option
WithoutIPTracking stops sessions from recording the client's address. Sessions and the session list then show no IP. Rate limits by IP still count: they hold the address for a window, not for the life of a session, and a limiter that forgot addresses would not limit anything.
func WithoutOriginCheck ¶
func WithoutOriginCheck() Option
WithoutOriginCheck turns off return-URL validation: TrustsReturnURL then accepts any target that is not empty and holds no line break or backslash, so every returnTo, errorTo, callbackURL and redirectTo is taken as given. It is better-auth's advanced.disableOriginCheck. Off by default; New logs WarnOriginCheckOff at every start while it is on. It does not touch the CSRF origin check (TrustsOrigin), which WithoutCSRFCheck controls.
The risks it opens: an open redirect, where a link on the real domain sends the victim to a look-alike page, which phishing relies on; and, worse, a token-bearing link to any host: a flow that puts a token in the URL it was given (a password-reset redirectTo, a magic-link or verification callback) mails the victim a link to the attacker's site with a live token in it, which is account takeover on one click. Every legitimate target stays reachable with the check on, through WithTrustedOrigins (including "myapp://" schemes and wildcard subdomains), WithTrustedOriginFunc or WithLoopbackReturn. Line breaks and backslashes stay refused: they guard the Location header against injection and URL confusion, not where the browser may go.
func WithoutServerCallRateLimits ¶
func WithoutServerCallRateLimits() Option
WithoutServerCallRateLimits exempts direct Go calls (Transport TransportGo: no transport attached a request) from ByIdentifier and ByFunc rules too, so a server call is never counted at all: better-auth's rule that auth.api calls skip rate limiting. Go calls already skip ByIP rules, since they carry no address. Off by default; New logs WarnServerCallLimitsOff at every start while it is on. Calls through authhttp and every adapter are still counted.
The risk it opens: an app route of its own that takes a password or a code from a browser and calls the operation from Go is then an unlimited guess channel against one account, which is exactly what the per-account rule exists to stop. Use it only when every Go call site is a trusted job (an import, a seed, a back-office tool), or put the app's own limiter in front of each route that forwards user input.
type OptionChecker ¶
type OptionChecker interface {
OptionErr() error
}
OptionChecker is implemented by a plugin or a component that collects option errors (usually through an OptionErrors field). New calls OptionErr on every plugin and on every component given to it through an option (rate store, session store, session cache, revocations, secondary storage, verification store, sealer, observer, tracer, message catalogue) before anything else, so a nil option inside a plugin or a contrib store fails start-up the same way a nil core option does.
type OptionErrors ¶
type OptionErrors struct {
// contains filtered or unexported fields
}
OptionErrors collects the mistakes options can only find while they run, above all a nil func or interface, so the component they configure fails start-up with an error naming the option instead of panicking at the first request that calls the nil value.
Why it exists: a functional option returns nothing, so it cannot fail by itself. Every option in this module and its plugin and contrib modules that takes a func or an interface records a nil here, and the error surfaces where the component is checked: New calls OptionErr on every plugin and on every component given to it through an option (see OptionChecker), and a constructor that returns an error returns it. One helper rather than a hand-written check per option, so the rule and its message are the same everywhere; the guard test TestGuard_OptionsRefuseNil finds every option that takes a func or an interface and fails if one does not call it.
The zero value is ready to use. Keep it in an unexported field and expose OptionErr through a one-line method, so the type's public surface gains only OptionErr.
func (*OptionErrors) JoinOptionErr ¶
func (o *OptionErrors) JoinOptionErr(err error)
JoinOptionErr adds err, which may be nil, to the collected errors. A composite option (one applying a nested list of options to a config of its own) passes the nested collector's OptionErr up with it.
func (*OptionErrors) OptionErr ¶
func (o *OptionErrors) OptionErr() error
OptionErr is every error the options recorded, joined; nil when none.
func (*OptionErrors) RefuseNil ¶
func (o *OptionErrors) RefuseNil(name string, v any) bool
RefuseNil records ErrNilOption naming the option name ("email.WithMailer") when v is nil, and reports whether it did, so an option reads
if p.optErrs.RefuseNil("email.WithMailer", m) {
return
}
and keeps its default. Nil means a nil interface, a nil func or a nil pointer (a typed nil inside an interface included): each would panic at its first call or dereference.
type OriginFunc ¶
OriginFunc decides, per request, whether an origin ("scheme://host[:port]") may send cookie-authenticated mutations. See WithTrustedOriginFunc.
type OriginTruster ¶
type OriginTruster interface {
TrustedOrigins() []string
}
OriginTruster adds origins allowed to send cookie-authenticated mutations, such as an OAuth provider's form-post callback. New reads TrustedOrigins before any plugin's Init, so the list must not depend on Init's state.
type Orphan ¶
type Orphan struct {
Kind OrphanKind
Name string
}
Orphan is something in the database that the current configuration does not own.
type OrphanKind ¶
type OrphanKind string
OrphanKind says what an Orphan is.
const ( // OrphanOwner is a migration owner in the ledger that no registered // plugin is: a plugin that was removed. Name is the owner. OrphanOwner OrphanKind = "owner" // OrphanTable is an auth_* table that neither the core nor a registered // plugin declares. Name is the table. OrphanTable OrphanKind = "table" // OrphanColumn is a column of auth_user that neither the core nor a // registered plugin declares. Name is the column. OrphanColumn OrphanKind = "column" )
Orphan kinds.
type OutboxResult ¶
type OutboxResult struct {
// Sent is how many messages the deliverers accepted; their rows are gone.
Sent int `json:"sent"`
// Failed is how many sends failed (the deliverer refused, the payload
// no longer opens, the kind has no deliverer); each row waits for its
// backoff, or stays for Sweep once its attempts are used up.
Failed int `json:"failed"`
}
OutboxResult counts what one SendOutbox call did.
type OwnerRefusalReporter ¶
type OwnerRefusalReporter interface {
OwnerRefused() error
}
OwnerRefusalReporter is an optional upgrade of TokenResolver: when its OwnerRefused returns an error, a token whose owner exists but is refused by a SessionChecker (an admin ban, an app's own check) answers that error instead of ErrSignedOut, as better-auth's API keys answer USER_BANNED. Returning nil keeps the safe default, where a refused owner and a missing one look alike. It costs one more query, only on that refusal.
type Ownership ¶
type Ownership struct {
Table sqlb.Table
ID sqlb.Expr[string]
Owner sqlb.Expr[string]
// NotFound replaces ErrResourceNotFound, for a plugin with its own code
// ("apikey.not_found").
NotFound error
// Forbidden, when set, is answered for a row that exists and belongs to
// another user, as better-auth answers FORBIDDEN. Left nil, that row is
// answered like a missing one (NotFound), which is the safe default:
// telling the two apart tells a caller which ids exist.
Forbidden error
}
Ownership says where a plugin table records who owns a row: the table, its id column and the column holding the owner's user id. Both columns are typed expressions of the plugin's own table (sqlb.Col[string]), so a misspelt column is a failed query in the plugin's own test, not a silent pass.
type Plugin ¶
type Plugin interface {
Name() string
}
Plugin is the only interface a plugin must satisfy.
Every other capability is an optional interface that New type-asserts for. An absent capability is then simply absent, instead of an empty method that looks the same as a forgotten one in a review.
Invariant: Name is stable, unique and matches KindNamePattern. It is the route prefix and the migration owner, so renaming a plugin orphans its recorded migrations in every deployment.
type Principal ¶
type Principal struct {
UserID string
// KeyID names the credential, for logs and for revoking it.
KeyID string
// Scopes are what the credential may do. The library only carries them;
// the app checks them with CurrentScopes.
Scopes []string
}
Principal is who a plugin-owned token acts for.
type PriorSignIn ¶
PriorSignIn is the previous sign-in a SignInHistory found.
type ProfileRequest ¶
type ProfileRequest struct {
Name *string `json:"name,omitempty"`
Image *string `json:"image,omitempty"`
// Fields sets the app columns WithProfileFields lists; any other name,
// or a value of the wrong type, is ErrFieldInvalid and nothing is
// written.
Fields Fields `json:"fields,omitempty"`
}
ProfileRequest changes the signed-in user's display fields; an omitted field is kept.
type ProvenSignIn ¶
type ProvenSignIn struct {
User User
// Session has SecondFactorPending set when a SecondFactor plugin requires
// a second factor; report that to the client rather than a sign-in.
Session Session
// Token is the new session's token. It is never written to JSON: the
// transport delivers it through the IssueSession effect, so an operation
// may return a ProvenSignIn as its output as it is.
Token Token `json:"-"`
// Created is true when this call created the user.
Created bool
}
ProvenSignIn is the result of Call.SignInProven.
type Purpose ¶
type Purpose string
Purpose names what a verification token or a delivered message is for, as "<plugin>.<name>", such as "email.verify" or "password.reset".
The purpose is part of every token lookup, so a verify token can never be used as a reset token.
const ( // PurposeVerify carries a link or code that proves control of an // identity. //ds:def id=core-purposes-wkjp4y8h owner=@khanakia stability=stable PurposeVerify Purpose = "verify" // PurposeReset carries a password reset link. PurposeReset Purpose = "reset" // PurposeSignUpAttempt tells the owner of a verified identity that // someone tried to sign up with it. It carries no link. PurposeSignUpAttempt Purpose = "sign_up_attempt" // PurposeChanged tells the old owner of an identifier that it was // replaced, with an undo link. PurposeChanged Purpose = "changed" // PurposeSignInCode carries a one-time sign-in code. PurposeSignInCode Purpose = "sign_in_code" // PurposeSignInLink carries a one-time sign-in link. PurposeSignInLink Purpose = "sign_in_link" // PurposeWelcome greets a new user once their first address is proven. //ds:def id=purposewelcome-addygcmp owner=@khanakia stability=stable PurposeWelcome Purpose = "welcome" )
Message purposes. A delivering plugin picks its template by these; a plugin with a message of its own declares its own Purpose beside them.
const PurposeDeleteAccount Purpose = "delete_account"
PurposeDeleteAccount is the purpose of the confirmation message a delivering plugin sends, and of the verification token in the link. One value for both because the token proves exactly what the message asked: "delete the account this link was sent for".
const PurposeReauth Purpose = "core.reauth"
PurposeReauth is the verification purpose of a re-authentication ticket: the first factor was proved on session Identifier, the second is due. It lives PendingSessionTTL, as a pending sign-in does.
const PurposeUnknownDevice Purpose = "unknown_device"
PurposeUnknownDevice is the purpose of the message sent when a user signs in from a device not seen before. Its Data is UnknownDeviceData, and it carries English DefaultSubject and DefaultText, so every delivering plugin (an SMS one too) has wording; an app's own templates, per locale through the delivering plugin (email.TemplatesByLocale), win over them.
type RateAction ¶
type RateAction string
RateAction says what a rule does with a call over its limit.
const ( // RateBlock refuses the call with ErrRateLimited (HTTP 429 and a // Retry-After), as every rule did before actions existed. RateBlock RateAction = "block" // RateLog lets the call through and reports it: the observer, every // RateEventListener plugin (plugin/audit) and the logger hear of it. It // is how a new limit is measured against real traffic before it is // turned into a block. RateLog RateAction = "log" // RateChallenge lets the call through only when it carries a proof the // RateChallenger accepts (a captcha, plugin/captcha, or a proof of work, // plugin/pow), and // otherwise answers ErrChallengeRequired, so a human behind a busy // address is asked to prove it rather than shut out. RateChallenge RateAction = "challenge" )
Rate actions. The zero value "" is RateBlock.
type RateChallenger ¶
RateChallenger decides whether a call over a RateChallenge rule carries an accepted proof: a captcha token, a proof of work, a fresh second factor. PassChallenge returns nil to let the call through, ErrChallengeRequired when the call carries no proof (so the client knows to ask for one), and another error, ideally an *Error, for a proof it rejects or cannot check; that error is the call's answer. It sees the Call before the session is resolved and, for ByIP rules, before the input is decoded, so it reads c.Header and c.Client.
plugin/captcha implements it with its Verifier, plugin/pow with a proof of work; registering either plugin is enough to make it the challenger, or WithRateChallenger sets another.
type RateChallengerFunc ¶
RateChallengerFunc adapts a function to RateChallenger.
func (RateChallengerFunc) PassChallenge ¶
func (f RateChallengerFunc) PassChallenge(ctx context.Context, c *Call) error
PassChallenge calls f.
type RateDecider ¶
type RateDecider interface {
Allow(ctx context.Context, key string, rule RateRule) (ok bool, retryAfter time.Duration, err error)
}
RateDecider is the optional upgrade of a RateStore that decides itself whether a call is within a rule, instead of returning a count the core compares with Limit. The rules' operations use Allow whenever the store implements it: key is the same hashed key Hit would get, and rule carries Limit and Window for the decider to interpret (as a bucket's capacity and refill period, for instance). retryAfter is what the caller is told to wait when ok is false; zero or less means the rule's Window. An error fails the call, as Hit's does.
type RateEvent ¶
type RateEvent struct {
Op OpInfo
Plugin string
Transport Transport
// Rule is the rule that was over its limit, with Action resolved (never
// ""). For WithRateLimit's blanket rule Match is the call's operation.
Rule RateRule
Outcome RateOutcome
// Count is the call's number in the rule's current window: Limit+1 for
// the first call over it. 0 when a RateDecider store decided, since a
// decider reports no count.
Count int
// Err is the error the caller got: ErrRateLimited (blocked),
// ErrChallengeRequired or the challenger's error (challenged); nil when
// the call went through (logged, challenge passed).
Err error
}
RateEvent describes one call over a rule's limit, whatever the rule's action did with it. It carries no input and no client address: like OperationEvent it may travel to a metrics sink. A RateEventListener plugin gets the Call beside it for anything more.
func (RateEvent) FirstInWindow ¶
FirstInWindow reports whether the event is the first over the limit in its window (Count is Limit+1), or one whose count is unknown (a RateDecider). The logger and plugin/audit record only these, so a flood against one key writes one line per window instead of one per request; the observer sees every event and can count them all.
type RateEventListener ¶
RateEventListener is a plugin that records calls over a rule's limit: plugin/audit writes one audit event per FirstInWindow event. It runs on the request's goroutine, before the operation's transaction exists, so a write goes to the database directly. It returns nothing: a listener's failure is its own to log and never changes the call's answer.
type RateExemptFunc ¶
RateExemptFunc decides, per call, whether the client is exempt from every ByIP rule, including WithRateLimit: a health checker, an internal address, a partner's gateway. It sees the Call before the input is decoded, so c.Input is nil; c.Client and c.Header are what it has. It must be fast and must answer false on its own failure, since a limiter that fails open is no limiter.
type RateKey ¶
type RateKey string
RateKey says what a rate rule counts by.
const ( // ByIP counts per client IP. Go API calls have no IP and are not counted. //ds:def id=core-ratekeys-n636dm3p owner=@khanakia stability=stable ByIP RateKey = "ip" // ByIdentifier counts per normalized identifier in the input, which stops // a distributed attack on one account. The input type must implement // Identified. ByIdentifier RateKey = "identifier" // ByFunc counts per whatever RateRule.KeyFunc returns for the call: the // signed-in user, the tenant, an API key, a device header. It runs after // the input is decoded and the caller resolved, beside ByIdentifier, so // c.Input, c.User and c.Session are set. Like every key, the value is // stored only as a hash. // //ds:def id=core-ratekey-byfunc-7nq4wz2k owner=@khanakia stability=stable ByFunc RateKey = "func" )
Rate keys.
type RateKeyFunc ¶
RateKeyFunc returns what a ByFunc rule counts the call by. "" means the call is not counted by that rule (a signed-out caller under a per-user rule, say). An error fails the call, as a rate store's error does, because a limiter that cannot tell who is calling must not wave the call through.
type RateLimiter ¶
type RateLimiter interface {
RateLimits() []RateRule
}
RateLimiter is a plugin that declares limits for its operations.
type RateObserver ¶
RateObserver is the optional upgrade of the WithObserver Observer that is also told of every call over a rule's limit: blocked, logged, challenged or passed. It runs on the request's goroutine and must be quick, like Observer.Operation.
type RateOutcome ¶
type RateOutcome string
RateOutcome is what happened to one call over a rule's limit.
const ( // RateOutcomeBlocked: a RateBlock rule refused the call. RateOutcomeBlocked RateOutcome = "blocked" // RateOutcomeLogged: a RateLog rule let the call through. RateOutcomeLogged RateOutcome = "logged" // RateOutcomeChallenged: a RateChallenge rule refused the call, because // it carried no proof (ErrChallengeRequired) or one the challenger // rejected (the challenger's error, such as captcha.ErrFailed). RateOutcomeChallenged RateOutcome = "challenged" // RateOutcomePassed: a RateChallenge rule let the call through because // its proof was accepted. RateOutcomePassed RateOutcome = "challenge_passed" )
Rate outcomes, one per RateEvent.
type RatePeeker ¶
type RatePeeker interface {
// Peek returns key's count in the window that contains now, 0 when
// there is none, without counting.
Peek(ctx context.Context, key string, window time.Duration) (int, error)
}
RatePeeker is a RateStore that can read a counter without adding to it. Every store the library ships implements it (memory, database, storage, contrib/redis); RatePeek answers ErrRatePeekUnsupported for one that does not, so a plugin that needs it refuses at Validate rather than counting wrong.
type RateRule ¶
type RateRule struct {
Match EndpointID
Key RateKey
Limit int
Window time.Duration
// Derive, when set, maps the key's value before counting, so a rule can
// count a group rather than one value: the first digits of a phone number
// count every number in a range, the domain of an address counts a whole
// provider. Optional; a rule without it counts the value itself.
Derive func(value string) string
// KeyFunc is what a ByFunc rule counts by, and is required by one; any
// other key must leave it nil, or New refuses the rule.
KeyFunc RateKeyFunc
// Action is what the rule does with a call over Limit: RateBlock (the
// default, also for ""), RateLog or RateChallenge. New refuses an
// unknown action, and a RateChallenge rule when no RateChallenger is
// registered.
Action RateAction
}
RateRule limits one operation.
type RateStore ¶
type RateStore interface {
Hit(ctx context.Context, key string, window time.Duration) (int, error)
}
RateStore counts hits. Hit adds one to key's counter for the current window of the given length and returns the new count.
The default counts in process memory. With more than one app instance each instance counts on its own, so an app that runs several passes a shared store; the first counted call logs a warning until it does.
A store whose algorithm is not a counter per window (a token bucket, GCRA, an external limiter's verdict) also implements RateDecider; Hit then serves only RateHit, a plugin's own counter.
func NewMemoryRateStore ¶
NewMemoryRateStore returns the in-process store, with its own clock so a test can move time forward.
func NewStorageRateStore ¶
func NewStorageRateStore(kv SecondaryStorage) RateStore
NewStorageRateStore returns a RateStore over kv: one fixed-window counter per key, created by the first hit with the window as its expiry, which is exactly the RateStore contract when Increment is atomic.
type ReauthAttempt ¶
type ReauthAttempt struct {
// SingleFactor is set by a method that can prove two factors but proved
// one this time (a passkey whose authenticator did not verify the user),
// as SessionOptions.SingleFactor is at sign-in.
SingleFactor bool
// MultiFactor is set by a method that proved two factors on its own (a
// user-verified passkey), as CreateMultiFactorSession is at sign-in: no
// second factor is asked. Exclusive with SingleFactor.
MultiFactor bool
}
ReauthAttempt says what the method that called Reauthenticate proved. The zero value is a one-factor proof (a password): the SecondFactor plugins decide, as they do at sign-in.
type Reauthentication ¶
type Reauthentication struct {
// Fresh is true when the session is fresh now: FreshSignIn operations
// pass until FreshUntil.
Fresh bool `json:"fresh"`
// FreshUntil is when the session stops being fresh (the
// re-authentication time plus WithFreshWindow); nil when not Fresh.
FreshUntil *time.Time `json:"freshUntil,omitempty"`
// SecondFactorRequired is true when the user's second factor is due
// before the session is fresh: the client sends Ticket with the code to
// the second factor's re-authentication operation.
SecondFactorRequired bool `json:"secondFactorRequired"`
// Ticket is the single-use proof of the first step, bound to this
// session and valid for PendingSessionTTL; "" unless
// SecondFactorRequired.
Ticket WireToken `json:"ticket,omitempty"`
}
Reauthentication is the answer of a re-authentication step.
type Request ¶
type Request struct {
Transport Transport
Client Client
Credential Credential
// Delivery is how the client asked to receive a token issued during this
// request (HTTP: the Auth-Token-Mode header). Empty means DeliveryCookie.
Delivery TokenDelivery
// Token is the raw session token, empty when Credential is CredentialNone.
Token Token
// Flow is the value the client holds from an earlier BindFlow effect, or
// empty. HTTP transports keep it in a cookie of its own.
Flow Token
// CachedSession is the value the client holds from an earlier
// EffectCacheSession, or empty. Only the snapshot cache uses one; a
// transport passes whatever the client sent, and the cache decides.
CachedSession string
// Host is the host the request came to, as the transport determined it
// (behind its trusted-proxy rule when it has one), such as
// "preview-42.example.com" or "app.example.com:8443"; empty for a
// transport without one. BaseURL(ctx) uses it only when WithAllowedHosts
// allows it, so a made-up Host header changes nothing.
Host string
// Device is the token the client holds from an earlier
// EffectRememberDevice, or empty (HTTP: a cookie of its own). A
// second-factor plugin reads it through RequestDevice to skip the second
// step on a remembered browser.
Device Token
// DeviceTokens are the other session tokens the device holds under
// WithMultiSession (authhttp: one cookie per account), without Token.
// The core lists and switches between them; a transport without the
// feature leaves it empty.
DeviceTokens []Token
// Header reads a request header. Nil for transports without headers.
Header func(name string) string
// Cookie reads a plugin cookie (Call.Cookie) by the name the plugin
// gave it, under whatever the transport names it on the wire; it
// reports whether the client sent one. Nil for transports without
// cookies.
Cookie func(name string) (string, bool)
}
Request is what a transport knows about one incoming request. A transport builds it once and passes it to Auth.WithRequest.
type Requirement ¶
type Requirement string
Requirement is what a call must have before the operation runs.
const ( // Public operations run for anyone, signed in or not: sign-in, sign-up. //ds:def id=core-requirements-tksmmmjf owner=@khanakia stability=stable Public Requirement = "public" // SignedIn operations need a valid session. SignedIn Requirement = "signed_in" // FreshSignIn operations need a session created within the fresh window, // for actions a stolen laptop must not be able to take: changing a // password or an email address. FreshSignIn Requirement = "fresh_sign_in" // PendingSecondFactor operations need a session that is waiting for a // second factor, and are the only ones that see such a session: the // operation that checks the code and calls PromoteSession. PendingSecondFactor Requirement = "pending_second_factor" // SignedInOrPending operations need a session, pending or full, and see // either: one operation serving both the sign-in step and the signed-in // user, such as sending a second-factor code. c.User is set in both // cases; the operation checks c.Session.SecondFactorPending itself when // the two cases differ. SignedInOrPending Requirement = "signed_in_or_pending" )
Requirements. The pipeline checks them, so an operation never needs its own "is anyone signed in" test and cannot forget one.
type RevokeSessionRequest ¶
type RevokeSessionRequest struct {
ID string `json:"id"`
}
RevokeSessionRequest is the input of core.sessions-revoke.
type RiskErrorMode ¶
type RiskErrorMode string
RiskErrorMode says what a check that returned an error does to the call.
const ( // RiskFailOpen lets the call through and logs a warning: a vendor outage // must not be a sign-in outage. The default. RiskFailOpen RiskErrorMode = "open" // RiskFailClosed refuses the call with ErrRiskCheckFailed (503) when the // check cannot answer, for an app that would rather lock people out than // let an unchecked sign-up through. RiskFailClosed RiskErrorMode = "closed" )
Error modes. The zero value "" is RiskFailOpen.
type RiskPolicy ¶
type RiskPolicy struct {
// Action is what a flag does: RateBlock (the default, 403), RateLog
// (record only, to measure a detector before enforcing it) or
// RateChallenge (the RateChallenger decides). A challenge needs a
// challenger, as for a rate rule.
Action RateAction
// OnError is what a check that returns an error does; see RiskErrorMode.
OnError RiskErrorMode
}
RiskPolicy is what a check does when it flags a call or fails. The zero value blocks a flagged call and fails open.
type RiskVerdict ¶
type RiskVerdict struct {
// Flagged is true when the check wants the policy's action applied.
Flagged bool
// Reason is a short machine-readable cause ("tor-exit", "datacenter",
// "headless-ua") recorded on the event as AttrReason. It is cut to
// MaxRiskReasonLen bytes, and must carry nothing secret or personal.
Reason string
}
RiskVerdict is a check's answer: whether it flags the subject, and why.
type SQLTypes ¶
type SQLTypes struct {
// ID is a column that holds a generated row id (Auth.NewID, or the
// app's WithIDs generator) or refers to one: every primary key the
// library generates, user_id, session_id, an organization's or a team's
// id. Override it to give ids a native type (UUID on Postgres). A column
// that may hold "" or a value the library did not generate (a provider's
// client id, a JWT id, a connection name) is a Key instead, because a
// native id type refuses those.
ID string
// Key is a short indexed text that is not a generated id: a kind, a
// purpose, a status, an operation id, a slug, a provider's subject.
Key string
// Text is a short display string.
Text string
// Value is an identifier value or an address.
Value string
// Long is a URL, a user agent or free text up to a few kilobytes.
Long string
// Timestamp holds a UTC instant with at least microsecond precision.
Timestamp string
// Bool is a flag; MySQL spells it BOOLEAN, which is TINYINT(1).
Bool string
// Hash is a 32-byte digest.
Hash string
// Bytes is a short binary value such as a WebAuthn credential id, at most
// 1024 bytes, and indexable on every engine.
Bytes string
// Doc is a large text value such as a JSON document, unindexed.
Doc string
// Int is a 64-bit integer: a counter or a numeric setting.
Int string
// TableSuffix ends every CREATE TABLE. On MySQL it sets the byte-exact
// collation.
TableSuffix string
}
SQLTypes is each engine's spelling of the few types auth tables use, so a plugin writing its own migration uses the same spellings as the core.
type Scannable ¶
Scannable is a destination that knows its own bindings: the app's composite user, which embeds auth.User and each plugin's column struct.
type SchemaColumn ¶
type SchemaColumn struct {
Name string
Type ColumnType
// Nullable is better-auth's required: false. A NOT NULL column added
// to a table with rows needs Default.
Nullable bool
// Default is a constant SQL literal ("”", "false", "0"), or empty.
Default string
// Unique adds a unique index on the column alone.
Unique bool
}
SchemaColumn is one column of a TableSchema. Type is a portable ColumnType; a text column takes the engine's indexable text type, so it can be unique or indexed on every engine.
type SchemaIndex ¶
type SchemaIndex struct {
// Name is unique within the table; the index is created as
// <real table name>_<Name>, so a renamed table renames its indexes.
Name string
Columns []string
Unique bool
}
SchemaIndex is an index over one or more columns of a TableSchema.
type SchemaReport ¶
type SchemaReport struct {
// PendingMigrations are the "owner/name" labels the ledger lacks, in
// the order Migrate would apply them.
PendingMigrations []string
// ChangedMigrations are applied migrations whose statements changed
// since; Migrate refuses to run while any exists.
ChangedMigrations []string
// MissingTables are tables the migrations create that the database
// lacks.
MissingTables []string
// MissingColumns are "table.column" pairs the migrations create that the
// database lacks, on tables that exist.
MissingColumns []string
}
SchemaReport is what CheckSchema found.
func (SchemaReport) Error ¶
func (r SchemaReport) Error() string
Error words the report for a log line or an operator; "" when OK.
func (SchemaReport) OK ¶
func (r SchemaReport) OK() bool
OK reports whether the database holds everything the configuration needs.
type Sealer ¶
type Sealer interface {
Seal(ctx context.Context, plain []byte, aad string) (string, error)
Open(ctx context.Context, sealed, aad string) ([]byte, error)
}
Sealer encrypts the secrets a plugin keeps at rest and must read back (provider refresh tokens, TOTP secrets): the seam for an app whose keys live in a KMS or HSM (Vault transit, AWS KMS, Google Cloud KMS) rather than in process memory. The default is the AES-256-GCM KeyRing (RingSealer); WithSealer supplies another for every plugin that seals through it.
Seal returns text that fits a text column. It is opaque to the core, so an implementation that rotates keys puts its key id inside it (Vault's "vault:v3:…" form) and Open picks the key from it; the KeyRing needs none, because it tries each of its keys. aad binds the value to its row, exactly as Seal's aad does: Open must refuse a value sealed under another aad. Open answers ErrSealedCorrupt (or an error wrapping it) for a value that does not open, and any other error for a failure to reach the key service; callers treat both as "no secret", never as an empty one.
func RingSealer ¶
RingSealer is the default Sealer: r's current key seals and every key opens, in the same format as KeyRing.Seal, so values sealed before an app switched to a Sealer still open.
type SecondFactor ¶
type SecondFactor interface {
SecondFactorRequired(ctx context.Context, userID string) (bool, error)
}
SecondFactor is a plugin that can require a second factor at sign-in, such as a TOTP plugin for users who enrolled.
type SecondFactorForSignIn ¶
type SecondFactorForSignIn interface {
SecondFactorRequiredForSignIn(ctx context.Context, in SignInAttempt) (bool, error)
}
SecondFactorForSignIn is an optional upgrade of SecondFactor: a plugin that implements it is asked with the whole SignInAttempt instead of the user id alone, so its policy can depend on how the user signed in (skip the factor after an SSO sign-in whose identity provider already enforced one) or from where. CreateSession calls it in place of SecondFactorRequired.
type SecondaryStorage ¶
type SecondaryStorage interface {
// Get returns the value under key and true, or false when there is none
// or it expired. An error is a failure of the store, not a miss.
Get(ctx context.Context, key string) (string, bool, error)
// Set stores value under key for ttl (0: no expiry), replacing any.
Set(ctx context.Context, key, value string, ttl time.Duration) error
// Delete removes key; an absent key is no error.
Delete(ctx context.Context, key string) error
// Increment adds one to the counter under key and returns the new value,
// atomically. An absent key starts at 1 and expires ttl later; later
// increments never extend that, so the counter is a fixed window.
Increment(ctx context.Context, key string, ttl time.Duration) (int64, error)
}
SecondaryStorage is one key-value store for the short-lived records, as better-auth's secondaryStorage is: implement these four methods over Redis, Memcached, Cloudflare KV or anything with expiring keys, and WithSecondaryStorage keeps sessions and rate-limit counters in it instead of the database. Values are strings; a ttl of 0 means no expiry.
Verification tokens are not in it, unlike better-auth's: they stay in SQL, where a conditional UPDATE spends each one exactly once inside the operation's transaction, so a KV store needs no atomic get-and-delete.
func NewMemoryStorage ¶
func NewMemoryStorage(now func() time.Time) SecondaryStorage
NewMemoryStorage returns a SecondaryStorage in this process's memory, on the given clock (nil: time.Now), for tests and single-process apps, as better-auth's in-memory secondary storage is. Expired keys read as absent and are dropped as they are read.
type SecurityEvent ¶
type SecurityEvent struct {
// ID is unique per event ("sev_" and 24 hex digits), for a sink that
// de-duplicates retries.
ID string
At time.Time
Kind SecurityKind
// Severity is set by the emitter; EmitSecurity fills SeverityInfo when
// it is empty.
Severity SecuritySeverity
// Outcome is what the library did: blocked, logged (recorded only),
// challenged or challenge_passed. The same four values as RateOutcome.
Outcome RateOutcome
// Op, Plugin and Transport name the operation the event happened in;
// "" for an event outside one.
Op EndpointID
Plugin string
Transport Transport
// UserID is the account concerned, "" when the event is about a call that
// named none (a rate rule by address).
UserID string
// IP is the client address, "" under WithoutIPTracking or outside a
// request.
IP string
// Attrs holds the kind's details under the Attr* keys.
Attrs map[string]string
}
SecurityEvent is one thing worth telling a security system about.
It carries no password, token, code or input, and the client address only while sessions track it (not under WithoutIPTracking), so a sink can ship it to a third party as it stands. Fields a kind does not know are zero.
type SecurityKind ¶
type SecurityKind string
SecurityKind names what a SecurityEvent reports. The closed set below is what the library emits; a plugin or an app may add kinds of its own through EmitSecurity, so a sink must tolerate a kind it does not know.
const ( // SecurityRateLimit: a call went over a rate rule's limit. Outcome says // what the rule did (blocked, logged, challenged, challenge_passed). One // event per rule window, like the audit log's. SecurityRateLimit SecurityKind = "rate.limit" // SecurityCredentialStuffing: plugin/password's GuardCredentialStuffing // saw one password fail on many accounts (or failures across all of them) // and crossed its limit. One event at the crossing, not one per refusal // after it. SecurityCredentialStuffing SecurityKind = "credential.stuffing" // SecurityLockout: a second factor locked after too many wrong codes // (plugin/twofactor's Lockout). One event when the lock is set, not one // per code refused while it holds. SecurityLockout SecurityKind = "lockout" // SecurityGeoCountry: a sign-in from a country WithGeoRules denies, or // outside the countries it allows. SecurityGeoCountry SecurityKind = "geo.country" // SecurityImpossibleTravel: a sign-in too far from the user's previous // one for the time between them (WithImpossibleTravel). SecurityImpossibleTravel SecurityKind = "geo.impossible_travel" // SecurityStaleAccount: plugin/stale warned an account that has not // signed in for its threshold, or deleted one after the grace period. // Outcome says which (warned, deleted). SecurityStaleAccount SecurityKind = "account.stale" // SecurityCompromisedPassword: a password check found the password in a // breach corpus (plugin/password/hibp). Never carries the password. SecurityCompromisedPassword SecurityKind = "password.compromised" // SecuritySuspiciousIP: the app's IP reputation check flagged the // caller's address. SecuritySuspiciousIP SecurityKind = "ip.suspicious" // SecurityBot: the app's bot check flagged the caller. SecurityBot SecurityKind = "bot" // SecurityTrialAbuse: the app's abuse check flagged a sign-up as a repeat // or fake account made to reuse a free trial. SecurityTrialAbuse SecurityKind = "trial.abuse" // SecurityReauthentication: a signed-in user proved who they are again // (Auth.Reauthenticate). AttrReauthStep says how it went: fresh (the // session is fresh), second_factor (the first factor passed, the code is // due) or failed (a proof was refused; SeverityWarn, Outcome blocked). // Successes are SeverityInfo with Outcome logged. SecurityReauthentication SecurityKind = "session.reauthenticated" // SecurityTokenReplay: a proof that is good only once was presented // again, such as a provider ID token plugin/oauth had already accepted // (its replay guard refused it). SeverityAlert, Outcome blocked: the // usual cause is a stolen token. AttrProvider names the provider. SecurityTokenReplay SecurityKind = "token.replay" // SecuritySessionTokenReuse: a session token that a rotation had // replaced was presented after its grace window (WithTokenRotation), // and the session was revoked. SeverityAlert, Outcome blocked: the // usual cause is a copied token. AttrSessionID names the session. //ds:def id=core-securitysessiontokenreuse-2kx7wm4q owner=@khanakia stability=stable SecuritySessionTokenReuse SecurityKind = "session.token_reuse" // SecuritySessionEndHookFailed: a DataHooks.SessionEnded hook returned an // error, so the session ended without that hook's work (a lost webhook, // a lost audit row). The ending itself always happens. AttrSessionID // names the session (empty for a user-wide end in stateless mode), // AttrReason the SessionEndReason and AttrError the hook's error text. SecuritySessionEndHookFailed SecurityKind = "session.end_hook_failed" )
Kinds the library emits.
type SecurityQueue ¶
type SecurityQueue struct {
// Name labels the sink in SecurityStats and in the log lines about it;
// "" is "sink1", "sink2", … in registration order ("log" for
// WithSecurityLog).
Name string
// Buffer is how many events may wait for the sink; 0 is
// DefaultSecurityBuffer.
Buffer int
// BatchSize is the most events handed to the sink in one call; 0 is
// DefaultSecurityBatch.
BatchSize int
// FlushEvery is the longest an event waits for its batch to fill; 0 is
// DefaultSecurityFlush.
FlushEvery time.Duration
}
SecurityQueue shapes one sink's queue. The zero value takes every default, and a negative number is refused at New.
type SecuritySeverity ¶
type SecuritySeverity string
SecuritySeverity ranks an event for a sink that pages someone on the high ones and only stores the rest.
const ( // SeverityInfo: expected traffic the app asked to see (a log-only rule, // a passed challenge). SeverityInfo SecuritySeverity = "info" // SeverityWarn: something was refused or challenged. SeverityWarn SecuritySeverity = "warn" // SeverityAlert: a pattern that looks like an attack or a stolen // credential (stuffing, impossible travel). SeverityAlert SecuritySeverity = "alert" )
Severities, lowest first.
type SecuritySink ¶
type SecuritySink interface {
WriteSecurity(ctx context.Context, batch []SecurityEvent) error
}
SecuritySink receives security events in batches. WriteSecurity runs on the sink's own goroutine, never on a request's, with a context bounded by SecurityWriteTimeout; it may block, retry and talk to the network. An error (or a panic, which is recovered) is logged and counted in SecurityStats, and that batch is not offered again: a sink that must not lose events retries inside WriteSecurity or writes to a durable queue of its own. The slice is reused after the call returns; keep a copy of anything you hold on to.
type SecuritySinkFunc ¶
type SecuritySinkFunc func(ctx context.Context, batch []SecurityEvent) error
SecuritySinkFunc adapts a function to SecuritySink.
func (SecuritySinkFunc) WriteSecurity ¶
func (f SecuritySinkFunc) WriteSecurity(ctx context.Context, batch []SecurityEvent) error
WriteSecurity calls f.
type Session ¶
type Session struct {
ID string
UserID string
ExpiresAt time.Time
LastSeenAt time.Time
// IP and UserAgent are for the "your sessions" list only. Both come from
// the client, so nothing may use them to decide anything.
IP string
UserAgent string
CreatedAt time.Time
// SecondFactorPending is true for a session whose user must still pass a
// second factor. Such a session signs nobody in: CurrentUser and the
// SignedIn requirement ignore it, and only PendingSecondFactor operations
// see it. PromoteSession replaces it with a full one.
SecondFactorPending bool
// Tasks are the session tasks still to complete (SessionTasker), empty
// for an ordinary session. While any is pending the session reaches only
// the operations its tasks allow, and CurrentUser reports nobody;
// CompleteSessionTask replaces it with one without the task.
Tasks SessionTasks
// ImpersonatedBy is the id of the admin acting as this user, or nil for
// the user's own session. It is on the row, so the user's session list
// shows it and nothing can hide that someone else was signed in as them.
ImpersonatedBy *string
// APIKeyID is set when the request was made with an API key rather than
// a session; such a Session is built for the request and has no row. It
// signs the user in for the app's routes, and every auth operation that
// needs a person refuses it.
APIKeyID string
// Transient is true for a session made without "remember me": it lives
// SessionConfig.TransientIdleTimeout at most, and its cookie has no
// expiry, so closing the browser ends it.
Transient bool
// IdleSeconds, RefreshSeconds and MaxExpiresAt are the lifetime chosen
// for this session when it was created (WithSessionPolicy), which a
// sliding refresh keeps using: a year-long app session must not shrink
// to the web default on its first refresh. 0 and nil mean the
// SessionConfig defaults, as for rows from before the columns existed;
// Lifetime resolves them.
IdleSeconds int64
RefreshSeconds int64
MaxExpiresAt *time.Time
// RevokedAt is when the session was ended before its expiry, kept under
// WithSessionArchive as an audit trail; nil for a live session. A
// revoked session is never found by a lookup or a list, only by
// RevokedSessions.
RevokedAt *time.Time
// ReauthenticatedAt is when the user last proved who they are on this
// session without signing in again (Auth.Reauthenticate), or nil when
// they never did; FreshAt is the later of it and CreatedAt, which is what
// a FreshSignIn operation checks. It never moves the expiry.
ReauthenticatedAt *time.Time
// Data is JSON the app attached to this session, or nil: the active
// workspace, a device name, a flag resolved at sign-in. Every store
// keeps it with the session, so a request reads it with no query
// (SessionData); SetSessionData replaces it, a Session.BeforeCreate
// hook sets what a session starts with (EncodeSessionData). It
// survives refresh, token rotation and the second-factor promotion,
// and ends with the session. At most MaxSessionDataSize bytes.
Data AppData
}
Session is a signed-in session. Its raw token is never in this struct or in the database; only its hash is stored.
func CurrentSession ¶
CurrentSession returns the session of the request in ctx, under the same rules as CurrentUser.
func (Session) FreshAt ¶
FreshAt is when the session last proved its user: the later of its creation and its last re-authentication. A FreshSignIn operation needs it within the fresh window.
func (Session) Lifetime ¶
func (s Session) Lifetime(c SessionConfig) SessionLifetime
Lifetime returns the lifetime the session was created with, falling back to c for a session from before lifetimes were stored (zero columns).
type SessionCache ¶
type SessionCache interface {
// Get returns the cached session and user for k, valid at now, or
// ok=false. An error is a failure of the cache itself, not a miss.
Get(ctx context.Context, k CacheKey, now time.Time) (s Session, u User, ok bool, err error)
// Put remembers a looked-up session until until. The returned string,
// when not empty, is handed to the client to send back on later requests
// as Request.CachedSession; the snapshot cache uses it, a server-side
// cache returns "".
Put(ctx context.Context, k CacheKey, s Session, u User, until time.Time) (clientValue string, err error)
// Drop forgets the entry for this token: sign-out on this device.
Drop(ctx context.Context, k CacheKey) error
}
SessionCache lets a request skip the session lookup for a short time. It sits in front of the store, or of stateless mode. A revocation reaches a cached request only when its entry expires, which is why the TTL is capped at MaxSessionCacheTTL and why a pending second-factor session is never cached.
The core passes its own clock as now and until, so a cache keeps no clock of its own and a test can move time. An entry never outlives the session's expiry: the core caps until at it.
func NewCompactSessionCache ¶
func NewCompactSessionCache(keys ...[]byte) SessionCache
NewCompactSessionCache returns a SessionCache like NewSnapshotCache that stores nothing on the server, but whose client copy is signed instead of encrypted, as better-auth's compact cookie-cache strategy is: the value is base64url JSON of the session, the user and the entry's expiry, a dot, and an HMAC-SHA256 over it and the token hash under the first key (at least SealKeySize bytes each; with none, the key New derives from WithSecret under KeyLabelCompactCache). More than one key is a rotation: the first signs, every one verifies, compared in constant time.
What it keeps from the sealed cache: a copy is used only with the token it was made for (the token hash is in the MAC, compared in constant time), only before its expiry, within the core's TTL bounds, and never renews itself: the core puts a new copy only after a store lookup. What it gives up: the claims are READABLE by anyone holding the value, which is the point when the front end wants the user without a request, and a reason to prefer NewSnapshotCache when the session carries app data (Session.Data), an impersonator's id or the client IP that the page's scripts should not see. Nothing in it is a credential.
func NewMemorySessionCache ¶
func NewMemorySessionCache(maxEntries int) SessionCache
NewMemorySessionCache returns a SessionCache in this process's memory, holding at most maxEntries; when full it drops the entry expiring soonest, which is an expired one whenever any is. Per instance, so a revocation on another instance is not seen until the entry expires; with several instances use a shared cache.
func NewSnapshotCache ¶
func NewSnapshotCache(keys ...[]byte) SessionCache
NewSnapshotCache returns a SessionCache that stores nothing on the server: Put seals the session and user, with the token hash and the expiry of the entry, under the first key (SealKeySize bytes each, AES-256-GCM) and hands the client the result; Get opens what the client sent back, with any of the keys, and accepts it only for the token it was sealed for, compared in constant time, so a snapshot is useless with another session's token. More than one key is a rotation: the first seals, every one opens (KeyRing). Drop has nothing to forget: the transport clears the client's copy through EffectClearCache.
type SessionCacheUserDropper ¶
type SessionCacheUserDropper interface {
// DropUser forgets every entry whose session belongs to userID. A user
// with no entry is not an error.
DropUser(ctx context.Context, userID string) error
}
SessionCacheUserDropper is a SessionCache that can forget every entry of one user at once. A server-side cache keeps the entries it hands out, so it can find a user's; the snapshot caches cannot, because the client holds the only copy, and do not implement it. The core asks for it after a write that must reach every device of the user before the TTL: a ban, a suspension, revoking sessions, deleting the user (EvictCachedSessions).
Entries are matched by Session.UserID. An entry Put concurrently with the drop, from a lookup that read the store before the write committed, may survive it; it then lives at most the cache TTL, which is the bound without eviction.
type SessionChecker ¶
SessionChecker adds conditions every session must meet, such as "the user is not banned". The predicates join the one session lookup query, so they may use only auth_user and auth_session columns.
type SessionConfig ¶
type SessionConfig struct {
IdleTimeout time.Duration
MaxLifetime time.Duration
// RefreshAfter limits how often an active session's expiry is written, so
// the middleware does not write to the database on every request.
RefreshAfter time.Duration
// TransientIdleTimeout replaces IdleTimeout for a session made without
// "remember me" (SessionOptions.Transient). Zero means
// DefaultTransientIdleTimeout.
TransientIdleTimeout time.Duration
}
SessionConfig holds the session lifetimes.
Two limits, because either alone is wrong: a session that never expires is a stolen laptop that never stops working, and one that expires an hour after sign-in annoys every user. IdleTimeout slides while the user is active; MaxLifetime is the hard stop after which they sign in again.
type SessionEnd ¶
type SessionEnd struct {
Session Session
Reason SessionEndReason
}
SessionEnd is what DataHooks.SessionEnded receives: one session that is ending and why. Session never carries the raw token.
In stateless mode (WithStatelessSessions) the server holds no row, so only the ends the core itself performs are reported: the request's own session (sign-out, revoke by id) and, for "sign out everywhere", one SessionEnd with an empty Session.ID and the user's id, meaning "every session of this user". Expiry is never reported there.
type SessionEndReason ¶
type SessionEndReason string
SessionEndReason says why a session ended before the user would have let it expire (or, for SessionEndExpired, that it ran out). The set is closed so a plugin can switch on it and a webhook consumer can rely on the strings; a path that fits none of them uses SessionEndUnspecified rather than a string of its own.
const ( // SessionEndUnspecified: the caller used DeleteSession or // DeleteUserSessions, which carry no reason. Prefer EndSession. SessionEndUnspecified SessionEndReason = "unspecified" // SessionEndSignOut: the signed-in user ended their own current session. SessionEndSignOut SessionEndReason = "sign_out" // SessionEndRevoked: the user ended one of their sessions by id. SessionEndRevoked SessionEndReason = "revoked" // SessionEndRevokedOthers: the user ended all their sessions but the // current one. SessionEndRevokedOthers SessionEndReason = "revoked_others" // SessionEndRevokedAll: the user ended every session they have // ("sign out everywhere"). SessionEndRevokedAll SessionEndReason = "revoked_all" // SessionEndAdmin: an administrator ended the session or all of a user's. SessionEndAdmin SessionEndReason = "admin_revoked" // SessionEndImpersonationStopped: an admin stopped impersonating, which // ends the impersonation session. SessionEndImpersonationStopped SessionEndReason = "impersonation_stopped" // SessionEndBanned: the user was banned or deactivated. SessionEndBanned SessionEndReason = "banned" // SessionEndPasswordChanged: the user changed their password. SessionEndPasswordChanged SessionEndReason = "password_changed" // SessionEndPasswordReset: the password was reset from a mailed token. SessionEndPasswordReset SessionEndReason = "password_reset" // SessionEndPasswordSet: a password was set on an account that had none. SessionEndPasswordSet SessionEndReason = "password_set" // SessionEndCredentialChanged: another credential change that ends // sessions created before it (a second factor turned on, an email // change undone). SessionEndCredentialChanged SessionEndReason = "credential_changed" // SessionEndTokenReuse: a rotated session token came back after its // grace, which is treated as theft (WithSessionTokenRotation). SessionEndTokenReuse SessionEndReason = "token_reuse" // SessionEndProviderLogout: a federated logout (OIDC end-session, SAML // single logout) ended the session. SessionEndProviderLogout SessionEndReason = "provider_logout" // SessionEndUserDeleted: the user was deleted. SessionEndUserDeleted SessionEndReason = "user_deleted" // SessionEndExpired: the session ran out and the sweep removed it. It // had stopped working already; the event lets a plugin clean up. SessionEndExpired SessionEndReason = "expired" )
Reasons a session ends.
type SessionExtras ¶
SessionExtras computes SessionResult.Extras for the signed-in call: the roles, the plan, whatever the front end needs with the session so it does not make a second request for it. It runs inside core.session with the call's user and session, after the requirement check, so c.User is set.
type SessionLifecycle ¶
type SessionLifecycle interface {
// SessionEnded is one session of userID ending by id: sign-out, revoke,
// an admin ending it, a device's other account being ended.
SessionEnded(ctx context.Context, userID, id string) error
// UserSessionsEnded is every session of userID ending but exceptID,
// which is "" for all: sign-out everywhere, a password reset, a ban.
UserSessionsEnded(ctx context.Context, userID, exceptID string) error
// SessionReplaced is a pending second-factor session (old) becoming the
// full session (new) at PromoteSession: a new id, the same sign-in.
// What the plugin kept for old belongs to new now.
SessionReplaced(ctx context.Context, old, new Session) error
}
SessionLifecycle is a plugin told when a session ends before its expiry, or is replaced, so what the plugin keeps per session can go with it. It exists because the library creates no foreign keys and no cascades (ADR-0002): a table keyed by session id is cleaned in Go, and these are the moments to do it. Every method runs inside the transaction that ends the session and before the store is told, so a plugin's error leaves the session and its rows untouched, whatever the store; a store's failure afterwards rolls the plugin's SQL back with it.
Expiry is not an end the core sees: a session that ran out is simply not found, and its rows are deleted by the plugin's own Sweeper. An end in stateless mode is only the cookie being cleared, except "sign out everywhere", which the core still reports.
type SessionLifetime ¶
SessionLifetime is how long one session lives: Idle slides with use, Max is the hard stop from creation, and RefreshAfter bounds how often the sliding expiry is written. A zero or negative field means the SessionConfig default for that field.
type SessionOptions ¶
type SessionOptions struct {
// Transient is "remember me" unticked.
Transient bool
// SingleFactor reports that this sign-in proved one factor whatever its
// operation, such as a passkey whose authenticator did not verify the
// user (possession only). It reaches SecondFactorForSignIn plugins as
// SignInAttempt.SingleFactor, so a policy that skips the factor for the
// method's other sign-ins can still ask for it here.
SingleFactor bool
}
SessionOptions are the per-sign-in choices a sign-in method passes to CreateSessionWithOptions.
type SessionPolicy ¶
type SessionPolicy func(ctx context.Context, in SessionPolicyInput) SessionLifetime
SessionPolicy chooses a session's lifetime at creation, per client. The choice is stored with the session, so every later refresh honours it.
func ByCredential ¶
func ByCredential(cookie, bearer SessionLifetime) SessionPolicy
ByCredential is the common policy: one lifetime for browsers (cookie) and one for apps (bearer), such as thirty days and a year. A Go call with no request gets the cookie lifetime, the shorter of the two in practice.
type SessionPolicyInput ¶
type SessionPolicyInput struct {
UserID string
// Credential is how the client presents its token: CredentialCookie for
// a browser, CredentialBearer for an app, CredentialNone for a Go call
// with no request in ctx. A request that asked for bearer delivery
// (Request.Delivery) reports CredentialBearer here too: at sign-in the
// app has no token yet, and it is the app's lifetime it needs.
Credential Credential
Client Client
// Transient is "remember me" unticked.
Transient bool
}
SessionPolicyInput is what a session policy sees when a session is created.
type SessionReauthStore ¶
type SessionReauthStore interface {
// SetReauthenticatedAt records at as session id's ReauthenticatedAt. An
// unknown or ended id is no error. Every other field, the expiry and the
// token above all, stays as it is.
SetReauthenticatedAt(ctx context.Context, id string, at time.Time) error
}
SessionReauthStore is the optional upgrade of a SessionStore that can record a re-authentication. The SQL, memory, storage-backed, mirror and contrib/redis stores implement it; a custom store without it answers ErrReauthUnsupported to every re-authentication, and its users sign in again as before.
type SessionResult ¶
type SessionResult struct {
User UserView `json:"user"`
ExpiresAt time.Time `json:"expiresAt"`
// Extras is what WithSessionExtras computed, or absent.
Extras any `json:"extras,omitempty"`
// NeedsRefresh is true, under WithDeferredSessionRefresh, when the
// expiry is due to slide and this read did not slide it: the client
// POSTs /session/refresh. Absent otherwise.
NeedsRefresh bool `json:"needsRefresh,omitempty"`
// Tasks are the session tasks the user must complete before the
// session is usable (TaskSetupMFA, or a task an app plugin declares);
// absent for an ordinary session. Until they are done every other
// operation answers auth.session_task_pending.
Tasks SessionTasks `json:"tasks,omitempty"`
}
SessionResult is the output of core.session.
type SessionRevocations ¶
type SessionRevocations interface {
// Revoke ends session id until until.
Revoke(ctx context.Context, id string, until time.Time) error
// Revoked reports whether session id is on the list at now.
Revoked(ctx context.Context, id string, now time.Time) (bool, error)
}
SessionRevocations is the revocation list of stateless sessions: the one piece of server state that lets a stateless token be ended before its expiry, one session at a time. A stateless token is otherwise valid until it expires, because the server keeps nothing about it; with a list, a sign-out, a revoke and the second-factor promotion add the session's id, and every stateless lookup (a cache hit included) refuses an id on it.
An entry is a session id, never the token or its hash: the id is not a credential, and every token of one session (a refresh reseals the same id) ends together. until is the latest instant any token of the session can still be valid, so an entry is needed no longer and an implementation lets it expire then (a Redis TTL, a pruned map); a lookup at or after until answers false whatever is still stored. Revoked's error is a failure of the list itself and fails the request: answering "not revoked" when the list cannot be read would let a revoked session in. github.com/ubgo/auth/sessiontest.RunRevocations checks the contract.
func NewMemorySessionRevocations ¶
func NewMemorySessionRevocations() SessionRevocations
NewMemorySessionRevocations returns a SessionRevocations in this process's memory. Per instance: a session revoked on one instance still opens on another, so with several instances use a shared list (contrib/redis). Entries past their instant are dropped by a lookup at most once a minute of the core's clock.
type SessionStore ¶
type SessionStore interface {
// Insert saves a new session under its token hash.
Insert(ctx context.Context, s Session, tokenHash []byte) error
// Find returns the session whose token hashes to tokenHash and that is
// still live at now (ExpiresAt after now), or ErrSessionNotFound.
Find(ctx context.Context, tokenHash []byte, now time.Time) (Session, error)
// Touch moves the expiry of session id forward to expiresAt, and records
// lastSeenAt, only if its current expiry is earlier; it reports whether
// it moved. Of concurrent refreshes the first wins and the rest report
// false.
Touch(ctx context.Context, id string, expiresAt, lastSeenAt time.Time) (bool, error)
// List returns userID's live, non-pending sessions: newest first, ties by
// id descending, the same order on every store.
List(ctx context.Context, userID string, now time.Time) ([]Session, error)
// Delete ends session id of userID; another user's id matches nothing.
Delete(ctx context.Context, userID, id string) error
// DeleteUser ends every session of userID except exceptID ("" = all).
DeleteUser(ctx context.Context, userID, exceptID string) error
// Sweep deletes sessions expired before now and reports how many; a
// store that expires entries by itself returns 0, nil.
Sweep(ctx context.Context, now time.Time) (int64, error)
// SetData replaces the app data (Session.Data) of session id; nil
// clears it. An unknown id is no error. Every other field, the expiry
// above all, stays as it is.
SetData(ctx context.Context, id string, data AppData) error
}
SessionStore persists sessions. The core keeps every rule — lifetimes, sliding expiry, the pending second factor, impersonation, data hooks — and a store only saves and finds. A store never sees a raw token, only its SHA-256 hash, so a store cannot leak a usable credential. A store keeps every field of the Session it is given, Data included: the conformance suite fails a store that drops one.
The SQL store (the default) writes through Auth.Exec, so it joins the transaction of ctx and a rolled-back sign-up leaves no session. Any other store does not: a rollback can leave it a session whose token never reached the client, because effects are dropped on rollback; that session expires by itself and nobody holds its token. github.com/ubgo/auth/sessiontest checks every method's contract; a custom store runs it before it ships.
func MirrorSessionStore ¶
func MirrorSessionStore(primary, secondary SessionStore) SessionStore
MirrorSessionStore keeps every session in two stores at once, as better-auth's storeSessionInDatabase does with secondary storage: the primary is authoritative (typically the database, through SQLSessionStore) and the secondary is the fast copy read first (typically Redis). Pass the result to WithSessionStore.
The consistency rule, chosen so that a failing secondary can cost speed but never let an ended session in:
- Find reads the secondary first and falls back to the primary on a miss or a secondary failure (logged); the secondary never has the last word on a miss, so a copy it lost or never got costs one primary read.
- Insert and Touch write the primary, which decides the result, then the secondary; a secondary failure is logged, never fails the request: a missing or older copy only sends a later Find to the primary.
- Delete and DeleteUser end the session in the secondary FIRST and fail the call when it fails: a copy left there would keep a revoked session working. Ending it in the primary second means a primary failure leaves it ended only in the copy, which is a fallback, not a sign-in.
- SetData writes the primary, then the secondary, and fails the call when the secondary fails, because a stale copy would answer with the old data; a primary that joins no transaction then already holds the new data, and the caller sees the error.
- SetReauthenticatedAt (SessionReauthStore) writes the primary, then the secondary, and fails the call when the secondary fails, as SetData does: Find reads the copy first, so a copy that missed it would answer "not fresh" right after the user was told the session is fresh. A stale copy never lets anything in; it only fails honestly. Either side without SessionReauthStore makes it ErrReauthUnsupported.
- List reads the primary alone (the copy may lack a session); Sweep runs on both and reports the primary's count, a secondary failure logged.
A mirror does not implement SessionUserFinder, so a request costs a Find and the user by primary key, and New refuses a SessionChecker that names auth_session columns. WithSessionArchive applies to an SQLSessionStore on either side. The mirror logs through the Auth's logger; New binds it.
func NewMemorySessionStore ¶
func NewMemorySessionStore() SessionStore
NewMemorySessionStore returns a SessionStore that lives in this process's memory: nothing survives a restart, and two instances never see each other's sessions. It is for tests and for a single-process app that accepts that every deploy signs everyone out. It joins no transaction; see SessionStore.
func NewStorageSessionStore ¶
func NewStorageSessionStore(kv SecondaryStorage) SessionStore
NewStorageSessionStore returns a SessionStore over kv; see storageSessionStore for the layout. Each record expires with its session, computed from the session's own times, never the wall clock, so a test clock and the store agree.
func SQLSessionStore ¶
func SQLSessionStore() SessionStore
SQLSessionStore names the default SQL store (the auth_session table) before New exists, so it can be one side of a MirrorSessionStore: New binds it to the Auth it is passed to, with that Auth's transaction and WithSessionArchive. Passed to WithSessionStore alone it is the default. Used before New binds it, every method answers ErrUnboundSessionStore.
type SessionTask ¶
type SessionTask struct {
ID SessionTaskID
// Allow lists the operations a session with this task pending may call,
// besides the core's session read, sign-out and refresh, which every
// task allows: the operations that complete the task and the reads they
// need. Every id must be an operation of this deployment (New refuses
// one that is not), and only these operations may call
// CompleteSessionTask for it.
Allow []EndpointID
}
SessionTask declares one task a plugin may require of a new session.
type SessionTaskError ¶
type SessionTaskError struct {
Tasks SessionTasks
}
SessionTaskError is ErrSessionTaskPending with the tasks the session still has, for a Go caller.
func (*SessionTaskError) Error ¶
func (e *SessionTaskError) Error() string
Error names the pending tasks.
func (*SessionTaskError) Unwrap ¶
func (e *SessionTaskError) Unwrap() error
Unwrap makes errors.Is(err, ErrSessionTaskPending) and ErrorOf work.
type SessionTaskID ¶
type SessionTaskID string
SessionTaskID names a session task: something a signed-in user must do before the session is usable, such as setting up a second factor or choosing an organization. It is stored on the session row and sent to the client in the session result, so it is stable wire vocabulary: lower case, at most 64 characters of letters, digits and "-", "_", ".", ":" (never a comma, which separates ids in the stored column).
const TaskSetupMFA SessionTaskID = "setup-mfa"
TaskSetupMFA is the task "set up a second factor before using the account". It is declared in the core, not in a plugin, because more than one plugin can complete it (a TOTP enrolment, a passkey) and a client screen keys on it; the plugin that requires it declares which of its operations are allowed while it is pending.
type SessionTasker ¶
type SessionTasker interface {
// SessionTasks declares every task the plugin may require, once at New.
SessionTasks() []SessionTask
// SessionTasksFor returns the tasks the session being created for in
// must complete, from those SessionTasks declared; none for a session
// that is usable at once. An error fails the sign-in.
SessionTasksFor(ctx context.Context, in SignInAttempt) ([]SessionTaskID, error)
}
SessionTasker is a plugin that can require tasks of a new session. A session with a task pending signs the user in for the operations its tasks allow and nothing else: every other operation answers ErrSessionTaskPending (as a *SessionTaskError naming the tasks), Public operations see a signed-out caller, and CurrentUser and CurrentSession report nobody, so an app's own routes treat the caller as signed out until the tasks are done. The client learns the tasks from the session result (SessionResult.Tasks).
Tasks are decided once, when the full session is created (after the second factor, when there is one); an impersonation session never gets any. A plugin completes its task with Auth.CompleteSessionTask.
type SessionTasks ¶
type SessionTasks []SessionTaskID
SessionTasks is the list of tasks a session still has to complete, in the order they were required. Empty for an ordinary session. It is stored in auth_session.pending_tasks as the ids joined by commas ("" for none), which is what Value and Scan convert.
func CurrentSessionTasks ¶
func CurrentSessionTasks(ctx context.Context) SessionTasks
CurrentSessionTasks returns the tasks the session of the request in ctx still has to complete, or nil for a signed-out request or an ordinary session. CurrentUser reports nobody while tasks are pending; an app's middleware calls this to send the user to the page that finishes them instead of to the sign-in page. Like CurrentUser it never queries.
func (*SessionTasks) Scan ¶
func (t *SessionTasks) Scan(src any) error
Scan reads the stored text: NULL and "" are no tasks. The bytes form is copied by the string conversion, so a reused driver buffer cannot change the result.
func (SessionTasks) String ¶
func (t SessionTasks) String() string
String is the ids joined by commas; none is "".
type SessionTokenRotator ¶
type SessionTokenRotator interface {
// RotateToken moves session id from oldHash to newHash, only while
// oldHash is still its current token hash and it is not ended: of
// concurrent rotations the first wins and the rest report false. It
// records oldHash as the previous hash, accepted until graceUntil
// (forgetting any earlier previous hash), and sets the expiry and
// last-seen as Touch would. Every other field stays as it is.
RotateToken(ctx context.Context, id string, oldHash, newHash []byte, graceUntil, expiresAt, lastSeenAt time.Time) (bool, error)
// FindRotated returns the live session (ExpiresAt after now, not ended)
// whose previous hash is tokenHash, with the time until which that
// token is accepted, or ErrSessionNotFound. The grace is not checked:
// a token found after it is the one the core treats as stolen.
FindRotated(ctx context.Context, tokenHash []byte, now time.Time) (Session, time.Time, error)
}
SessionTokenRotator is the optional upgrade of a SessionStore that WithTokenRotation needs: it moves a session to a new token hash while remembering the one it replaced, and finds a session by that replaced hash. The SQL, memory and contrib/redis stores implement it; New refuses WithTokenRotation with a store that does not, and nothing changes for such a store without the option. sessiontest.Run checks it when a store has it.
type SessionUserFinder ¶
type SessionUserFinder interface {
SessionStore
// FindWithUser is Find plus the user, refused (ErrSessionNotFound) when
// the user fails any of checks.
FindWithUser(ctx context.Context, tokenHash []byte, now time.Time, checks []sqlb.Predicate) (Session, User, error)
}
SessionUserFinder is a SessionStore that can also load the session's user, with every plugin's SessionChecker predicates applied, in its own query. The SQL store implements it: one statement joins auth_session to auth_user, which is what a request costs. A store without it costs two: Find, then the user by primary key with the predicates on auth_user alone — so such a store cannot serve a SessionChecker that names auth_session columns, and New refuses one.
type SessionView ¶
type SessionView struct {
ID string `json:"id"`
Current bool `json:"current"`
// Impersonated is true for a session an admin opened as this user.
Impersonated bool `json:"impersonated"`
IP string `json:"ip"`
UserAgent string `json:"userAgent"`
CreatedAt time.Time `json:"createdAt"`
LastSeenAt time.Time `json:"lastSeenAt"`
}
SessionView is one row of core.sessions.
type SessionsResult ¶
type SessionsResult struct {
Sessions []SessionView `json:"sessions"`
}
SessionsResult is the output of core.sessions.
type SetIfAbsenter ¶
type SetIfAbsenter interface {
SecondaryStorage
// SetIfAbsent stores value under key for ttl (0: no expiry) and returns
// true, or returns false and changes nothing when a live value is
// already there.
SetIfAbsent(ctx context.Context, key, value string, ttl time.Duration) (bool, error)
}
SetIfAbsenter is a SecondaryStorage that can store a key only when it is absent, in one atomic step: Redis SET NX. It is what lets the key-value verification store refuse a duplicate token (ExclusiveVerificationStore).
type SignInAttempt ¶
type SignInAttempt struct {
UserID string
// Client is the request's client (IP, user agent, origin).
Client Client
// Operation is the operation signing the user in, such as
// "password.sign-in" or "oauth.callback"; "" for a direct Go call to
// CreateSession outside any operation.
Operation EndpointID
// Method is the name of the plugin that registered Operation, such as
// "password", "oauth" or "sso": the sign-in method. "" when Operation is.
Method string
// SingleFactor is set by a method that proved one factor on this
// sign-in although the operation can also prove two (a passkey without
// user verification), through SessionOptions.SingleFactor. False tells a
// policy nothing beyond Operation and Method.
SingleFactor bool
// Reauthentication is true when the user is not signing in but proving
// who they are on the session they hold (Auth.Reauthenticate). A policy
// that decides by Operation already sees the re-authentication
// operation; the flag is for one that decides by Method alone.
Reauthentication bool
}
SignInAttempt describes the sign-in a session is being created for, as a SecondFactorForSignIn plugin sees it. It is a struct so fields can be added without breaking implementers.
type SignInHistory ¶
type SignInHistory interface {
PreviousSignIn(ctx context.Context, a *Auth, userID string) (prior PriorSignIn, found bool, err error)
}
SignInHistory answers "where and when did this user last sign in?" for impossible-travel detection. The default reads the user's sessions (live and, under WithSessionArchive, revoked ones); an app that keeps a login table, or runs stateless sessions, passes its own with WithTravelHistory. found false means there is nothing to compare with (a first sign-in), and no check is made. An error is logged and the sign-in goes ahead.
func SessionSignInHistory ¶
func SessionSignInHistory() SignInHistory
SessionSignInHistory is the default SignInHistory: the newest session of the user that carries an address, among the live ones and the archived revoked ones. Pending sessions (a second factor not yet passed) and impersonation sessions are not the user's own sign-ins. A user whose sessions have all expired and been swept has no history; keep WithSessionArchive for longer memory.
type SignInHistoryFunc ¶
SignInHistoryFunc adapts a function to SignInHistory.
func (SignInHistoryFunc) PreviousSignIn ¶
func (f SignInHistoryFunc) PreviousSignIn(ctx context.Context, a *Auth, userID string) (PriorSignIn, bool, error)
PreviousSignIn calls f.
type SignInMethod ¶
type SignInMethod interface {
SignsIn() bool
}
SignInMethod is a plugin that can sign a user in. New refuses a plugin list in which none reports true, because such an app has no way for anyone to get in. A method rather than a marker so a plugin whose sign-in is optional, such as phone codes, can say that this configuration does not sign anyone in.
type SignUpIdentifier ¶
type SignUpIdentifier struct {
Kind IdentifierKindName
Value string
}
SignUpIdentifier is one identifier of a new account, normalized.
type SignUpInfo ¶
type SignUpInfo struct {
// UserID is the new user's id.
UserID string
// Identifiers are the account's first identifier, normalized (the
// address a "+tag" or dot-trick checker compares).
Identifiers []SignUpIdentifier
// Client is the caller's address and user agent; empty for a Go call.
Client Client
}
SignUpInfo is what a TrialAbuseChecker sees of a new account.
type SinkStats ¶
type SinkStats struct {
Name string
// Queued counts events accepted into the queue.
Queued uint64
// Delivered counts events WriteSecurity accepted (returned nil for).
Delivered uint64
// Dropped counts events refused because the queue was full or the stream
// was closed.
Dropped uint64
// Failed counts events in batches WriteSecurity failed or panicked on;
// they are not offered again.
Failed uint64
}
SinkStats counts one sink's events since New.
type Span ¶
type Span interface {
End(err error)
}
Span is one started span. End reports how it ended: nil for success, else the error the call or hook returned, which an adapter records by code (ErrorOf) and never by text, so a database error's SQL stays out of the trace the same way it stays out of the response.
type SpanInfo ¶
type SpanInfo struct {
Kind SpanKind
Name string
Op OpInfo
Plugin string
Transport Transport
Stage HookStage
Source string
// Route is the request path a middleware span wraps; empty otherwise.
Route string
// DBOperation and Table are set for database and data-hook spans: the
// statement's verb ("select", "insert", "update", "delete", or "query"
// for anything else) and the real table name it names first, or for a
// data hook its stage ("before_create" …) and the core model ("user",
// "identity", "session", "verification").
DBOperation string
Table string
}
SpanInfo describes a span to a Tracer at its start. Name is ready to show ("auth.operation core.session", "auth.hook before core.session app") and the other fields are its attributes, typed so an adapter maps them to its own attribute keys without parsing the name. Stage and Source are set for hook spans only.
type SpanKind ¶
type SpanKind string
SpanKind names what a span measures.
const ( SpanOperation SpanKind = "operation" SpanHook SpanKind = "hook" SpanMiddleware SpanKind = "middleware" SpanDatabase SpanKind = "db" SpanDataHook SpanKind = "db_hook" )
The spans the core starts. An operation span wraps one call from the first pipeline check to the result; a hook span wraps one Before or After hook inside it. Both carry the operation, so a trace shows which hook made a sign-in slow.
A middleware span wraps one plugin's net/http middleware around a request (authhttp.HTTPMiddlewarer), better-auth's onRequest/onResponse spans; it carries the plugin and the route, and no operation.
A database span wraps one statement the core or a plugin runs through Exec, One or Each (better-auth's "db {operation} {model}"), and a data-hook span one DataHooks function; both carry DBOperation and Table, and the operation the call is running when there is one. No span carries SQL or arguments: only the statement's verb and its table.
type Statement ¶
Statement is a sqlb builder ready to render: every Insert, Update, Delete and Select a sqlb constructor returns satisfies it.
Exec, One and Each take the builder rather than a rendered sqlb.Query so the render step and its error live in one place (prepare), not after every statement: a fixed statement shape never fails to render, and a branch per call site for that would be code no input can reach.
type SweepResult ¶
SweepResult counts what one Sweep deleted, by owner and table, as "core.auth_session" or "phone". The table part is the table's label, not its real name under RenameTable or WithTablePrefix, so an app that reads the result keys sees the same keys in every deployment.
type Sweeper ¶
Sweeper is a plugin with rows of its own that expire, such as SMS codes. Sweep deletes those that stopped mattering before cutoff and returns how many it deleted.
type TableHooks ¶
type TableHooks[T any] struct { BeforeCreate func(ctx context.Context, tx sqlb.DBTX, row *T) error AfterCreate func(ctx context.Context, tx sqlb.DBTX, row *T) error BeforeUpdate func(ctx context.Context, tx sqlb.DBTX, old, new *T) error AfterUpdate func(ctx context.Context, tx sqlb.DBTX, old, new *T) error BeforeDelete func(ctx context.Context, tx sqlb.DBTX, row *T) error }
TableHooks run around writes to one core table, inside the writing transaction. A Before hook may change the row or refuse the write; an error from any hook rolls the whole transaction back.
Every hook receives tx and must use it: a hook that wrote through the database handle instead would escape the rollback.
type TableOwner ¶
TableOwner declares the tables a plugin brings into statements, so New can detect an alias collision between plugins whose authors never met.
type TableSchema ¶
type TableSchema struct {
Table sqlb.Table
ID string
Columns []SchemaColumn
Indexes []SchemaIndex
}
TableSchema describes a plugin's table: a text primary key named ID, the columns and the indexes.
type Token ¶
type Token string
Token is a raw secret: a session token, a verification token, a reset code.
Why it is its own type: a secret that is a plain string ends up in an error message, a log line or a fmt.Printf sooner or later. A Token prints as "[redacted]" through every formatting path Go has — String, GoString, the slog LogValuer and fmt's %v and %+v — so the careless path is the safe one. Reading the value is possible only through Reveal, whose name makes each use easy to find in a review.
func NewToken ¶
func NewToken() Token
NewToken returns a new random token, base64url-encoded without padding so it is safe in a cookie, a URL and a header without further escaping. It cannot fail: see randomBytes.
func RequestDevice ¶
RequestDevice returns the device token the client sent with the request in ctx, from an earlier EffectRememberDevice (HTTP: a cookie of its own), or "" outside a request or when the client holds none. The core gives it no meaning: the plugin that issued it (two-factor "remember this browser") hashes and checks it.
func (Token) Format ¶
Format hides the value from every other fmt verb, including %q and %x, which do not go through String.
func (Token) MarshalJSON ¶
MarshalJSON writes "[redacted]", so a result struct that carries a Token can never leak it through JSON encoding. A transport that must send the token reads it from the issue-session effect and calls Reveal.
type TokenDelivery ¶
type TokenDelivery string
TokenDelivery names how the client wants to receive a session token: in a cookie, or in the response for it to send back as a bearer token. It is asked for before any token exists, at sign-in, which is why it is not Credential: Credential says how a token WAS presented on this request, and the origin check and the session lookup depend on that meaning.
const ( DeliveryCookie TokenDelivery = "cookie" DeliveryBearer TokenDelivery = "bearer" )
How a client asks to receive its token. The session policy treats a bearer request like a bearer credential, so an app gets the app lifetime on the sign-in that has no token yet.
type TokenPrefixLister ¶
type TokenPrefixLister interface {
TokenPrefixes() []string
}
TokenPrefixLister is an optional upgrade of TokenResolver for a plugin whose tokens come in several prefixes (API key configurations "sk_live_", "sk_test_"). When implemented, the core routes a token starting with any of them to the plugin, and TokenPrefix is not consulted. Prefixes of one plugin may overlap each other (the plugin finds its keys by hash); they may not overlap another plugin's.
type TokenResolver ¶
type TokenResolver interface {
TokenPrefix() string
// ResolveToken returns the principal behind t, or ErrSignedOut for a
// token that is unknown, expired or revoked.
ResolveToken(ctx context.Context, t Token) (Principal, error)
}
TokenResolver is a plugin that owns bearer tokens with a prefix of its own, such as API keys ("ubk_…"). A request whose bearer token starts with the prefix is resolved by the plugin instead of the session table.
Only bearer tokens are routed here: a browser's session cookie is never an API key, so a cookie that happens to carry the prefix is not trusted.
type TokenRotation ¶
type TokenRotation struct {
// Grace is how long the token a rotation replaced is still accepted;
// 0 means DefaultTokenRotationGrace, and more than
// MaxTokenRotationGrace or a negative value is refused by New.
Grace time.Duration
}
TokenRotation configures WithTokenRotation.
type Tracer ¶
Tracer starts spans. It is the dependency-free seam for distributed tracing: the core module may depend on nothing but sqlb, so it describes spans and an adapter (contrib/otel) turns them into OpenTelemetry spans. Start returns the context the rest of the work runs in, so a child span an adapter creates is the parent of everything inside the call: the hooks, and the app's own spans started from a hook or an instrumented *sql.DB.
Tracer and Observer are deliberately two things: the Observer sees every call end with its duration for metrics and error counting, whether or not a trace is being sampled; the Tracer sees the structure inside the call.
type TrialAbuseChecker ¶
type TrialAbuseChecker interface {
CheckSignUp(ctx context.Context, s SignUpInfo) (RiskVerdict, error)
}
TrialAbuseChecker judges a new account: a repeat sign-up made to reuse a free trial, a throwaway address, a farm of accounts behind one address. It runs when the user's first identifier is stored.
type TrialAbuseCheckerFunc ¶
type TrialAbuseCheckerFunc func(ctx context.Context, s SignUpInfo) (RiskVerdict, error)
TrialAbuseCheckerFunc adapts a function to TrialAbuseChecker.
func (TrialAbuseCheckerFunc) CheckSignUp ¶
func (f TrialAbuseCheckerFunc) CheckSignUp(ctx context.Context, s SignUpInfo) (RiskVerdict, error)
CheckSignUp calls f.
type TypedMigrator ¶
TypedMigrator is the upgrade of Migrator for a plugin whose DDL spells every type through the SQLTypes it is handed, which are TypesFor(engine) with the app's WithSQLTypes overrides applied. When a plugin implements it, Migrations calls it instead of Migrator's method. New refuses WithSQLTypes for the app's engine while any plugin implements only Migrator, because that plugin's tables would silently keep the built-in types beside core tables that changed (a UUID user id joined to a TEXT one fails on Postgres).
type UnknownDeviceData ¶
type UnknownDeviceData struct {
UserAgent string `json:"userAgent"`
IP string `json:"ip,omitempty"`
At time.Time `json:"at"`
SessionID string `json:"sessionId"`
// Method is the sign-in method, the plugin of the operation that signed
// the user in ("password", "oauth"), or "" for a Go call outside one.
Method string `json:"method,omitempty"`
}
UnknownDeviceData is Message.Data for PurposeUnknownDevice: what an app template shows about the sign-in. IP is "" under WithoutIPTracking.
type User ¶
type User struct {
// ID is the stable public identifier, and the foreign-key target for plugin
// tables and app tables alike. It is opaque: nothing may parse it.
ID string
// Name and Image are display fields, set at sign-up or imported from a
// social provider. Both may be empty.
Name string
Image string
CreatedAt time.Time
UpdatedAt time.Time
// SessionsValidAfter is set by DeleteUserSessions with no exception
// ("sign out everywhere"): a session created before it is refused in
// every session mode, which is what revokes stateless sessions, whose
// tokens the server never stored. Nil until the first such call.
SessionsValidAfter *time.Time
}
User is the core identity record: the minimum any sign-in needs.
Why it is this thin: every column here is a column every consuming app carries forever, whether it uses email, phones, handles or only Google. An email address is not here, because an app that signs in with a phone has none; the email plugin adds its own columns to auth_user. The test for adding a field is whether an app with no plugins but one sign-in method would be wrong without it.
func CurrentUser ¶
CurrentUser returns the signed-in user of the request in ctx.
It returns false both for a signed-out request and for a context that is not a request at all, and for a session that is not usable yet: a pending second factor, or session tasks pending (CurrentSessionTasks names them). It never queries the database by itself: the session must have been resolved by middleware or by an operation, which keeps a helper called in a loop from turning into a query per call.
type UserTable ¶
type UserTable struct {
// Table is one the plugin declares through TableOwner; New refuses one
// it does not, because the registry would then name a table nobody
// creates.
Table sqlb.Table
// Column holds the user id, as "user_id".
Column string
}
UserTable is a table with a column that holds a user id: the rows whose Column equals a user's id belong to that user and are deleted with the user.
type UserTableOwner ¶
type UserTableOwner interface {
UserTables() []UserTable
}
UserTableOwner is a plugin with tables whose rows belong to users. It lists them so DeleteUser removes a user's rows from each, in plugin order, before the core's own rows and the user. A plugin table with a user column that is not listed keeps its rows after the user is gone; the library's test suite refuses such a table (TestGuard_UserTablesAreDeleted), and a plugin in another repository should carry the same check.
type UserUpdate ¶
UserUpdate names the display fields to change; a nil field is kept.
type UserView ¶
type UserView struct {
ID string `json:"id"`
Name string `json:"name"`
Image string `json:"image"`
}
UserView is the part of a user the core returns to a client. A separate type so a field added to User for internal use never reaches a response.
type Verification ¶
type Verification struct {
ID string
// Identifier names what the token is about. Which id it holds depends on
// Purpose, and each purpose's owner documents it: an identity id for an
// email verification, a user id for a password reset, a provider name for
// an OAuth state.
Identifier string
Purpose Purpose
ExpiresAt time.Time
ConsumedAt *time.Time
// Meta is free text owned by the purpose's plugin, such as an OAuth PKCE
// verifier. Empty for most purposes.
Meta string
CreatedAt time.Time
}
Verification is a single-use token row: a verify link, a reset link, an OAuth state. The raw token is never stored.
type VerificationSender ¶
VerificationSender is an identifier plugin that can send a fresh verification (a link or a code) for one of its unverified identities, on request from another plugin. The password plugin asks for one when a sign-in with the right password is refused because the address is not verified yet, so the person gets the mail they need instead of a dead end.
type VerificationStore ¶
type VerificationStore interface {
// Insert saves v under tokenHash.
Insert(ctx context.Context, v Verification, tokenHash []byte) error
// Consume spends the token of purpose whose hash is tokenHash, live at
// now, and returns its row with ConsumedAt set, or ErrTokenInvalid.
Consume(ctx context.Context, purpose Purpose, tokenHash []byte, now time.Time) (Verification, error)
// ConsumeAll spends every open token of purpose filed under identifier.
ConsumeAll(ctx context.Context, purpose Purpose, identifier string, now time.Time) error
// DeleteIdentifiers ends every token filed under any of identifiers,
// whatever its purpose, issued up to now: what DeleteUser calls for the
// user's id and its identities' ids and values.
DeleteIdentifiers(ctx context.Context, identifiers []string, now time.Time) error
// Sweep deletes tokens that expired, or were spent, before cutoff, and
// returns how many. A store that expires records itself returns 0.
Sweep(ctx context.Context, cutoff time.Time) (int64, error)
}
VerificationStore is where single-use verification tokens live: email links, reset links, magic links, OAuth state, passkey ceremonies. The SQL store on auth_verification is the default; WithVerificationStore replaces it, as better-auth's secondaryStorage holds verification records.
A store keeps a token's hash, never the token. The contract every store keeps, checked by github.com/ubgo/auth/verificationtest:
- Consume spends a token exactly once: of any number of concurrent Consume calls with one hash, exactly one returns the row and every other returns ErrTokenInvalid.
- A wrong, spent, expired (ExpiresAt not after now) or wrong-purpose token is ErrTokenInvalid, all alike, and a wrong-purpose attempt does not spend the token.
- ConsumeAll ends every open token of one purpose for one identifier and no other; DeleteIdentifiers ends every token, of any purpose, filed under any of the identifiers.
What a store outside the database gives up: it joins no transaction. With the SQL store, a token spent by an operation that then fails is unspent again by the rollback, and a token issued by one that fails never existed. A key-value store keeps both: the spent token stays spent (the user asks for a new link) and the issued one stays until it expires, although nobody received it, because delivery runs only after a commit. Sweep has nothing to delete there either: the store's own expiry removes each record.
func NewMemoryVerificationStore ¶
func NewMemoryVerificationStore(now func() time.Time) VerificationStore
NewMemoryVerificationStore returns a VerificationStore in this process's memory, on the given clock (nil: time.Now), for tests and single-process apps.
func NewStorageVerificationStore ¶
func NewStorageVerificationStore(kv GetAndDeleter) VerificationStore
NewStorageVerificationStore returns a VerificationStore over kv; see storageVerificationStore for the layout. Pass it to WithVerificationStore.
type WireToken ¶
type WireToken string
WireToken is a secret that is meant to cross the wire in a request or response body: the token an invite hands its creator, the poll token a cookieless client keeps, the link token a Go client sends back.
Why it exists beside Token: Token encodes as "[redacted]" in JSON on purpose, so a struct that is logged or stored by accident never leaks it. That same rule blanked every response field that was meant to hand a token to the caller, and made a Go client that JSON-encodes a request send "[redacted]" instead of the token it holds. WireToken encodes as its value in JSON, and only in JSON: String, GoString, LogValue and every fmt verb still print "[redacted]".
Invariant: auth.New refuses an operation whose output carries a Token in a JSON-visible field (see checkOutputTokens), because such a field always answers the constant "[redacted]". Use WireToken there, or `json:"-"` when the value travels through an effect instead.
Source Files
¶
- account.go
- auth.go
- call.go
- code.go
- columns.go
- constants.go
- cookies.go
- datahooks.go
- dberr.go
- detect.go
- device.go
- effects.go
- env.go
- errorcode.go
- errors.go
- escapes.go
- geo.go
- hosts.go
- identifier.go
- import.go
- keyring.go
- lifetime.go
- locale.go
- maintain.go
- messages.go
- migrate.go
- multisession.go
- newdevice.go
- observer.go
- operation.go
- optionerr.go
- origins.go
- orphans.go
- outbox.go
- ownership.go
- pagesize.go
- pipeline.go
- plugin.go
- rateaction.go
- ratehit.go
- ratelimit.go
- ratestore_db.go
- reauth.go
- returnurl.go
- risk.go
- schema.go
- schemacheck.go
- scope.go
- seal.go
- secevent.go
- secret.go
- session.go
- sessioncache.go
- sessioncache_compact.go
- sessiondata.go
- sessionend.go
- sessionmirror.go
- sessionrevoke.go
- sessionstore.go
- sessiontask.go
- sessionwire.go
- signupfields.go
- stateless.go
- storage.go
- store.go
- sweep.go
- tables.go
- tableschema.go
- token.go
- tokenresolver.go
- tokenrotation.go
- trace.go
- tx.go
- types.go
- usertables.go
- verificationstore.go
- wire.go
- wiretoken.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package authcli is the command line an app gives its operators for the auth schema: apply migrations, print them as SQL, or show what is pending.
|
Package authcli is the command line an app gives its operators for the auth schema: apply migrations, print them as SQL, or show what is pending. |
|
Package authgate is a login gate, not user accounts: sign in with any OAuth 2.0 or OpenID Connect provider and keep the person's profile in a sealed cookie, with no database, no user table and no auth.New.
|
Package authgate is a login gate, not user accounts: sign in with any OAuth 2.0 or OpenID Connect provider and keep the person's profile in a sealed cookie, with no database, no user table and no auth.New. |
|
Package authhttp serves auth operations over net/http.
|
Package authhttp serves auth operations over net/http. |
|
Package authtest is the test harness for applications that embed the library: an Auth on a fresh database per test, a clock the test moves, a mailbox and an SMS box that record what the plugins send, a signed-in session as a cookie, a bearer header or a Go context, and a browser with a cookie jar over the app's handler.
|
Package authtest is the test harness for applications that embed the library: an Auth on a fresh database per test, a clock the test moves, a mailbox and an SMS box that record what the plugins send, a signed-in session as a cookie, a bearer header or a Go context, and a browser with a cookie jar over the app's handler. |
|
authui
module
|
|
|
contrib
|
|
|
echo
module
|
|
|
fiber
module
|
|
|
gin
module
|
|
|
gqlgen
module
|
|
|
otel
module
|
|
|
phonenumbers
module
|
|
|
redis
module
|
|
|
Package i18n ships ready translations of every error message the library and its plugins declare, in the 21 languages better-auth's i18n plugin ships beside English, as an auth.Messages catalogue:
|
Package i18n ships ready translations of every error message the library and its plugins declare, in the 21 languages better-auth's i18n plugin ships beside English, as an auth.Messages catalogue: |
|
plugin
|
|
|
admin
Package admin gives an app's operators the user operations every product needs: find a user, see their identities, ban and unban, end their sessions, and grant or remove admin rights.
|
Package admin gives an app's operators the user operations every product needs: find a user, see their identities, ban and unban, end their sessions, and grant or remove admin rights. |
|
anonymous
Package anonymous gives a visitor an account before they have told the app who they are: a guest session for a cart, a trial or a draft, and a link to the real account the moment they sign up or sign in.
|
Package anonymous gives a visitor an account before they have told the app who they are: a guest session for a cart, a trial or a draft, and a link to the real account the moment they sign up or sign in. |
|
apikey
Package apikey gives users long-lived keys for scripts and integrations: "Authorization: Bearer ubk_…" in place of a session.
|
Package apikey gives users long-lived keys for scripts and integrations: "Authorization: Bearer ubk_…" in place of a session. |
|
audit
Package audit records security events: every mutation operation that ran, successful or refused, with who did it, to whom, from where, and how it ended.
|
Package audit records security events: every mutation operation that ran, successful or refused, with who did it, to whom, from where, and how it ended. |
|
captcha
Package captcha refuses chosen operations unless the request carries a captcha token a provider accepts, so a bot cannot sign up, sign in or ask for a reset link at the rate the rate rules allow a human to.
|
Package captcha refuses chosen operations unless the request carries a captcha token a provider accepts, so a bot cannot sign up, sign in or ask for a reset link at the rate the rate rules allow a human to. |
|
email
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.
|
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. |
|
emailchallenge
Package emailchallenge signs users in without a password by one challenge that a link and a code both complete, and that always signs in the browser that asked, never the one that opened the mail: better-auth-email-challenge.
|
Package emailchallenge signs users in without a password by one challenge that a link and a code both complete, and that always signs in the browser that asked, never the one that opened the mail: better-auth-email-challenge. |
|
handoff
Package handoff moves a signed-in session to another client in one step: a web page hands a native app a short-lived, single-use token, and the app exchanges it for a session of its own.
|
Package handoff moves a signed-in session to another client in one step: a web page hands a native app a short-lived, single-use token, and the app exchanges it for a session of its own. |
|
invite
Package invite is invitation codes for users, better-invite's plugin: a signed-in user creates an invite, either public (a link anyone holding it may use, up to a number of uses) or private (mailed to one address, used once), with an expiry and optionally a role; every use is recorded with who used it and when; and Required makes sign-up refuse anyone without a valid invite.
|
Package invite is invitation codes for users, better-invite's plugin: a signed-in user creates an invite, either public (a link anyone holding it may use, up to a number of uses) or private (mailed to one address, used once), with an expiry and optionally a role; every use is recorded with who used it and when; and Required makes sign-up refuse anyone without a valid invite. |
|
jwt
Package jwt issues short-lived JSON Web Tokens from a session, for other services that must know who is calling without sharing the auth database.
|
Package jwt issues short-lived JSON Web Tokens from a session, for other services that must know who is calling without sharing the auth database. |
|
jwt/jwtpreset
Package jwtpreset holds example jwt.Template conventions for third-party services that accept a token from your app: Supabase, Hasura, Convex and Firebase custom tokens.
|
Package jwtpreset holds example jwt.Template conventions for third-party services that accept a token from your app: Supabase, Hasura, Convex and Firebase custom tokens. |
|
lastlogin
Package lastlogin remembers how each user last signed in — password, a provider, a magic link, a code, a passkey — so a sign-in page can put the method the person used last in front, and so an app can see which methods its users actually use.
|
Package lastlogin remembers how each user last signed in — password, a provider, a magic link, a code, a passkey — so a sign-in page can put the method the person used last in front, and so an app can see which methods its users actually use. |
|
magiclink
Package magiclink signs users in with a single-use link sent to one of their identifiers, usually an email address.
|
Package magiclink signs users in with a single-use link sent to one of their identifiers, usually an email address. |
|
metadata
Package metadata keeps three JSON documents per user, the shape Clerk made familiar:
|
Package metadata keeps three JSON documents per user, the shape Clerk made familiar: |
|
oauth
Package oauth signs users in with identity providers such as Google and GitHub.
|
Package oauth signs users in with identity providers such as Google and GitHub. |
|
oauth/oauthtest
Package oauthtest checks an oauth.Provider against the contract the oauth plugin relies on, by driving it against a fake authorization server it starts.
|
Package oauthtest checks an oauth.Provider against the contract the oauth plugin relies on, by driving it against a fake authorization server it starts. |
|
phone
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.
|
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. |
|
pow
Package pow is a proof-of-work challenge: a client proves it spent CPU time before a call that a rate rule found suspicious is let through.
|
Package pow is a proof-of-work challenge: a client proves it spent CPU time before a call that a rate rule found suspicious is let through. |
|
sessiondata
Package sessiondata keeps per-session app data in a table of its own, keyed by session id, for an app that wants the data out of the session record: a value too big for a cookie, a value the session store (Redis, memory) should not hold, or a table the app joins and reports on.
|
Package sessiondata keeps per-session app data in a table of its own, keyed by session id, for an app that wants the data out of the session record: a value too big for a cookie, a value the session store (Redis, memory) should not hold, or a table the app joins and reports on. |
|
stale
Package stale finds accounts nobody has signed in to for a long time, mails their owners, and, if the app asks for it, deletes the accounts that never answered.
|
Package stale finds accounts nobody has signed in to for a long time, mails their owners, and, if the app asks for it, deletes the accounts that never answered. |
|
terms
Package terms records which version of the terms each user accepted, and when, and lets the app refuse users who have not accepted the current one.
|
Package terms records which version of the terms each user accepted, and when, and lets the app refuse users who have not accepted the current one. |
|
twofactor
Package twofactor adds TOTP second factors (authenticator apps) with single-use recovery codes.
|
Package twofactor adds TOTP second factors (authenticator apps) with single-use recovery codes. |
|
username
Package username adds handle-based identity to the auth core.
|
Package username adds handle-based identity to the auth core. |
|
access
module
|
|
|
agentauth
module
|
|
|
deviceauth
module
|
|
|
deviceauth/oauthdevice
module
|
|
|
ldap
module
|
|
|
oauthprovider
module
|
|
|
oauthproxy
module
|
|
|
organization
module
|
|
|
password
module
|
|
|
referral
module
|
|
|
samlidp
module
|
|
|
scim
module
|
|
|
siwe
module
|
|
|
sso
module
|
|
|
Package sessiontest checks a SessionStore against the contract every session store must keep, so a custom store proves itself with one call rather than by finding out in production which rule it broke.
|
Package sessiontest checks a SessionStore against the contract every session store must keep, so a custom store proves itself with one call rather than by finding out in production which rule it broke. |
|
Package verificationtest checks a VerificationStore against the contract every verification store must keep, so a custom store proves itself with one call rather than by finding out in production that a reset link worked twice.
|
Package verificationtest checks a VerificationStore against the contract every verification store must keep, so a custom store proves itself with one call rather than by finding out in production that a reset link worked twice. |
