authhttp

package
v0.1.0 Latest Latest
Warning

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

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

README

github.com/ubgo/auth/authhttp

Serves every auth operation as a JSON route on net/http. It also gives your own handlers the signed-in user, sets the session cookie or bearer token, and turns errors into JSON.

Install

go get github.com/ubgo/auth

It is part of the core module and uses only the standard library, so it adds no dependency. Any router that accepts an http.Handler (the standard mux, chi, gorilla/mux) mounts it directly. gin, echo and fiber have their own adapters in contrib/, and those adapters are built on this package.

Wire it

From examples/quickstart:

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

mux := http.NewServeMux()
mux.Handle("/auth/", authhttp.Handler(a, authhttp.Prefix("/auth")))
mux.Handle("/me", authhttp.Middleware(a)(authhttp.RequireUser(http.HandlerFunc(me))))

func me(w http.ResponseWriter, r *http.Request) {
	u := authhttp.MustUser(r)
	_ = json.NewEncoder(w).Encode(u.ID)
}

auth.WithBaseURL matters here: its origin is the only one, apart from auth.WithTrustedOrigins, allowed to send cookie-authenticated POSTs.

Options

These apply to Handler, Middleware and every building block below. Pass the same ones everywhere, or two parts of the app will disagree about which cookie holds the session.

Option What it does Default
Prefix(p) Path the handler is mounted at, such as "/auth"; a trailing slash is dropped. It must match the pattern the handler is registered under. "" (routes at the root)
Logger(l) Where internal errors are logged. Their text never reaches the client. slog.Default()
CookieDomain(domain) Shares the session with every subdomain of domain, and renames the cookies to DomainCookieName and DomainFlowCookieName. Every subdomain can then read and set the session, so use it only when every subdomain is yours and trusted. host-only cookies
CookieDomainFromBaseURL() CookieDomain with the domain taken per response from the host of a.BaseURL(ctx) for that request, as better-auth derives the cookie domain from a dynamic base URL: a request to pr-42.preview.example.com under auth.WithAllowedHosts("*.preview.example.com") sets its cookies for that host and its subdomains. The host is one WithAllowedHosts accepted, or the static base URL's, so a forged Host header cannot choose the scope. Cookies are named like CookieDomain's (__Secure-); an explicit CookieDomain wins. host-only cookies
CookiePrefix(prefix) Replaces the auth in every cookie name: CookiePrefix("myapp") names them __Host-myapp_session, __Host-myapp_flow, __Host-myapp_session_cache, __Host-myapp_device and __Host-myapp_account_… (__Secure- under CookieDomain), for two apps on one host or a cookie policy that names its cookies. The __Host-/__Secure- prefix cannot be removed. The prefix must be a cookie-name token (no spaces, separators or control characters) or the option panics, because net/http would otherwise drop every Set-Cookie silently. DefaultCookiePrefix, auth
RenameCookie(role, name) Names one cookie, better-auth's advanced.cookies.<cookie>.name: RenameCookie(CookieSession, "sid") sets the session cookie as __Host-sid; roles are CookieSession, CookieFlow, CookieCache, CookieDevice. The browser prefix and every attribute stay. An unknown role or a name that is not a cookie-name token panics, and so does Handler/Middleware/Begin when two cookies end up with one name. Pass it everywhere, as with CookiePrefix every name from the prefix
WeakenCookies(attrs), WeakenCookie(role, attrs) better-auth's defaultCookieAttributes and cookies.<cookie>.attributes, as an escape hatch: CookieAttributes ScriptReadable drops HttpOnly (one XSS bug hands over the session), Insecure drops Secure and the __Host-/__Secure- prefix (plain-HTTP development; the cookie crosses the network in clear text), CrossSite sends SameSite=None (the origin check is then the only CSRF defence; refused with Insecure), Path narrows the path and names the cookie __Secure-. Logged as a warning at every start. every protection on
SameSiteStrict() Sends the session, cache, device and account cookies with SameSite=Strict instead of Lax: a top-level navigation from another site arrives with no session. The flow cookie stays Lax whatever this says, because the OAuth return is such a navigation and a Strict one would never come back. Trade-off: browsers treat a redirect chain that started at the provider as cross-site, so the first page after an OAuth callback may render signed out until the next navigation. Lax
PartitionedCookies() Adds Partitioned (CHIPS) to every cookie, for an app embedded in another site's page: one jar per top-level site. off
Health() Adds GET <prefix>/ok answering {"ok":true} (HealthPath), for a liveness probe or a post-deploy smoke test. Opt-in, so an existing route table gains nothing it did not ask for. It reads no database: green means mounted and running, not ready to sign people in. off
Reference(opts) Adds GET <prefix>/reference (ReferencePath): an HTML page that loads an OpenAPI viewer and points it at the document OpenAPI serves, so the wired routes can be browsed and tried from a browser. It needs OpenAPI; Handler panics at start-up without it. ReferenceOptions: ScriptURL names the viewer script (empty: Scalar's standalone bundle from jsDelivr, pinned, DefaultReferenceScript; an app that will not load a third-party script serves the bundle itself), Nonce is called per request and its answer becomes the nonce attribute of both script tags for a CSP that allows scripts by nonce (the app's middleware sets the policy header). Theme is one of the viewer's named themes (ReferenceThemeValues, better-auth's list; anything else panics at start-up), sent as the viewer's data-configuration. A development convenience, opt-in like OpenAPI. off
ErrorPage(opts) Adds GET <prefix>/error (ErrorPagePath), better-auth's /api/auth/error: the page a failed browser flow lands on when auth.WithErrorURL points at it. It shows the error code when it is shaped like one (unknown otherwise), the code's declared message in the request's language when the library declares it, and the provider's error_description, escaped and capped. Its title, heading and link translate through auth.WithMessages under ErrorPageTitleCode, ErrorPageHeadingCode and ErrorPageBackCode, and its lang follows the request's locale when the heading is translated (en otherwise). ErrorPageOptions: Template replaces the page with any html/template over ErrorPageData, Redirect sends the request on to the app's own page with the same parameters (it must pass TrustsReturnURL, or Handler panics at start-up), Nonce puts a CSP nonce on the built-in page's style. The page is no-store. off
OpenAPI(info) Adds GET <prefix>/openapi.json (OpenAPIPath): the OpenAPI 3.1 document of every operation this handler serves, built once from the same operation list as the routes, with request and response schemas derived from the operations' Go types, the requirement as security (sessionCookie or bearerToken), one error schema for every failure, and Health and AcceptForms reflected when set. A field, and a query parameter, is required unless its json tag has omitempty, the declaration every input type uses for what a caller may leave out; a ,string field is a string and a type with its own MarshalJSON is any JSON. OpenAPIInfo carries the title, version, description and server URLs. Opt-in like Health; the document is public because it describes routes a client can discover anyway. off
TrustedProxies(networks...) Believes X-Forwarded-For, then X-Real-IP (or the headers ClientIPHeaders names) only when the connection comes from one of the given CIDRs or addresses, taking the rightmost forwarded entry that is not itself a trusted proxy. Any other connection keeps its own address, so a client that writes the header gains nothing. A value that is not a network panics at start-up. none: the connection's address
ClientIPHeaders(names...) The headers a trusted proxy's client address is read from, in order, for a proxy that writes CF-Connecting-IP or Fly-Client-IP. Naming HeaderForwarded reads the standard Forwarded header (RFC 7239) by its for= parameters, quotes, IPv6 brackets and ports removed, with the same rightmost-untrusted rule; it is never read by default. No effect without TrustedProxies, for Forwarded as for every other header. X-Forwarded-For, X-Real-IP
ClientIPFunc(fn) Replaces how the client's address is found, for a platform whose proxy rules TrustedProxies cannot express (an address in a context value, a signed header). fn owns the trust decision, so it must not return a value a client controls without checking where the request came from. An answer that is not an IP address ("" included) falls back to the connection's address and the TrustedProxies rules, so text never reaches rate-limit keys or audit rows. off
EncodeErrors(enc) Writes every error body in the app's own shape, such as RFC 9457 application/problem+json, through an ErrorEncoder func(w, r, info auth.ErrorInfo). The library still decides the rest: info is already the 500 with no detail for an error that is not an *auth.Error, the response carries info.Status whatever status the encoder writes (an encoder that writes nothing still answers it), and Retry-After is set again after the encoder returns. It applies to Handler (its 404 and 405 included), OperationHandler, Middleware, and the guards behind it (RequireUser, RequireFunc, RequireScope), and through them to the gin, echo and fiber adapters. {"error": info}
AcceptForms() Lets a Mutation take an application/x-www-form-urlencoded body as well as JSON, one value per field, so a plain HTML form posts to a route without JavaScript; lists and objects stay JSON-only. off: a form body is a bad request
SkipTrailingSlash() Routes /auth/sign-out/ as /auth/sign-out. off: exact routes
LocaleCookie(name) Reads the locale from the named cookie (DefaultLocaleCookieName, auth_locale, when name is empty) before Accept-Language, reduced like a header value; the app sets the cookie. off: Accept-Language only
LocaleFunc(fn) Asks fn(r) for the locale first; a non-empty answer, reduced like a header value, wins over the cookie and Accept-Language, an empty or unusable one falls through. For a source the transport cannot know: the app's own cookie or header, a tenant's default, a preference the app looks up itself. It runs before the session lookup, because the lookup's own error is rendered in the locale, so a preference stored on the user is reachable only through what the request carries (LocaleFunc). off
APIKeyHeader(name) Lets an API key arrive in the named header ("X-Api-Key") as well as in Authorization: Bearer. The value is handed to the core as a bearer credential, so every rule for a key there (no cookie, no origin check, scopes) applies; a session cookie, then Authorization, win over it. off: Authorization only
APIKeyFrom(get) Reads an API key from wherever your clients send it (a query parameter, say), as better-auth's customAPIKeyGetter. Runs only when there is no session cookie, no Authorization header and no APIKeyHeader value; the key gets every rule a bearer key gets. A key in a URL reaches access logs, so prefer a header. off

Handlers, middleware and helpers

API What it does
Handler(a, opts...) Serves every registered operation at prefix plus the operation's route: a Query as GET (input from the query string, one value per key), a Mutation as POST (JSON body; an empty body is the zero input; unknown fields are refused). An unknown path answers 404 auth.unknown_operation. A known path with the wrong method answers 405 auth.method_not_allowed with an Allow header. A failed operation that asked for a redirect on error (Call.RedirectOnError) is answered with that 302 instead of the error JSON.
OperationHandler(a, op, opts...) Serves one operation, with the same decoding, effects and error JSON as Handler. It is for a router that registers each route itself.
MethodOf(kind) The HTTP method for an operation kind: GET for auth.Query, POST otherwise.
OpenAPIDocument(a, info, opts...) The bytes OpenAPI serves, for a generator that writes them to a file: the repository's task docs:api writes docs/reference/openapi.json with every plugin wired and task check fails when it is stale. Pass the same options as Handler so the prefix matches.
TypeScriptClient(a) The files of a typed TypeScript client for every operation a serves, from the same operation list: client.ts (a type per Go input and output, an operation table and factory per plugin, createAuthClient), runtime.ts (the fetch runtime, embedded from tsclient/runtime.ts), error-codes.ts, extensions.ts (the hand-written oauthPopup() and lastLoginMethod(), embedded from tsclient/extensions.ts) and index.ts (TSFileClient and the other names). A plugin's methods exist on the client only when the plugin is passed to it. ErrTypeScriptName refuses a name two things would share. The repository's task docs:api writes clients/ts/src with every plugin wired; an app runs auth client ts. Guide: TypeScript client.
TypeScriptMethods(a) The methods of that client as data (TSMethod: namespace, name, operation id, route, requirement, input and output types), core first; it fails exactly when TypeScriptClient does. The repository's task docs:api writes docs/reference/ts-client.md from it.
TypeScriptErrorCodes() The error-code module of the client, docs/reference/error-codes.ts byte for byte: every code auth.Errors() lists, with status, message and owning plugin, the AuthErrorCode union and the isAuthErrorCode guard.
Middleware(a, opts...) Attaches the request to the core and resolves the session once, for your own handlers and for a GraphQL handler. Effects recorded by operations called inside the handler (such as an issued session) become headers before the handler's first byte. A session lookup that fails for any reason other than "signed out" answers 500 instead of running the handler as if nobody were signed in.
RequireUser(next) Answers 401 auth.signed_out when there is no session. It must run inside Middleware.
RequireFunc(check) Refuses the request when check(r) returns an error, answering with that error's JSON. Use it for your own rule, such as a role or who owns a resource. It must run inside Middleware.
RequireUserExcept(next, allow...) RequireUser for a whole handler tree, deny by default: every path needs a signed-in user except the listed ones. An entry ending in / opens that subtree ("/auth/"), any other entry exactly that path ("/login"). It must run inside Middleware.
RequireScope(scope) Lets through only a request that carries an API key with scope. A request with no principal at all (no credential, or a revoked, expired or unknown key) answers 401 auth.signed_out, so the client knows to authenticate. A principal without the scope answers 403 auth.missing_scope, and so does a browser session, because a scope-guarded route is for keys. No RequireUser is needed in front of it. It must run inside Middleware.
HTTPMiddlewarer (plugin interface) A plugin that returns PluginMiddleware values from HTTPMiddleware() wraps Handler's routes in net/http middleware, the Go form of a better-auth plugin's onRequest, onResponse and path-matched middlewares: Wrap(next) may replace the request, answer without calling next, or wrap the writer to see the response; Paths limits it to exact routes or prefixes ending in *, after the prefix (empty: every request Handler receives). Plugins apply in order, the first outermost, each run traced as auth.middleware <plugin> <route> under auth.WithTracer. Only Handler applies them, not OperationHandler or a GraphQL call, so a rule every entry point must keep belongs in an auth.OperationHook.
CurrentUser(r) The signed-in user, and false when signed out. It never queries the database.
MustUser(r) The signed-in user. It panics with ErrNoUser when there is none, for handlers behind RequireUser.
WriteError(w, err) Writes err as the error JSON with its status, plus Retry-After for a rate-limited error. An error that is not an *auth.Error answers 500 with no detail.
WriteErrorFor(w, r, err) WriteError with the message in the request's locale when the app set auth.WithMessages and r went through Begin, Middleware or Handler; code and status are unchanged, and without a catalogue so is the message. Every error Handler, RequireUser and RequireFunc answer goes through it; under EncodeErrors the body is the encoder's.
WriteErrorWith(w, r, err, opts...) WriteError for an adapter that holds the request and the options but no context from Begin yet (because Begin failed): the EncodeErrors encoder when opts or r's context set one, else the error JSON. The message is not translated, as WriteError's is not.
ErrorEncoderOf(ctx, opts...) The EncodeErrors encoder opts set, else the one ctx carries from Begin, Middleware or Handler, else nil; for an adapter whose framework has no http.ResponseWriter to hand out (fiber).
SetRetryAfter(w, err) Sets Retry-After in whole seconds when err carries a retry time. It is for a transport that writes errors in its own format, as authui does.
UIMethod (plugin interface), SignInMethod, SignInMethods(a) A plugin that offers sign-in buttons to the pages an app renders implements UIMethod (SignInMethods() []SignInMethod): each method has an ID, a Kind for styling, an English Label, the operation Route it starts (a GET Query that redirects, joined by the page to where Handler is mounted) and optionally one Field the page asks for first, such as an email for an SSO start. SignInMethods(a) collects them in plugin order, for an app's own sign-in page; authui renders them. It lives here, not in authui, so a plugin implements it without importing authui.
RequestOf(r, opts...) Converts an *http.Request into the core's auth.Request: cookie, bearer token or API-key header, client IP, user agent, origin, locale, Sec-Fetch-Site (Client.FetchSite, acted on only under auth.WithFetchMetadata), the host the request came to (Request.Host: the Host header, or the first X-Forwarded-Host entry when the connection is from a TrustedProxies network; the core acts on it only under auth.WithAllowedHosts), the flow cookie, the device token, the device's other accounts and the client's cached session copy.
Begin(a, r, opts...) Attaches r and resolves its session, returning the request with its new context. Every adapter starts a request with it.
HTTPRequest(ctx) The *http.Request a call arrived on, for an operation, a hook or a handler behind Middleware (better-auth's ctx.request; r.URL.Query() is ctx.query); false for a direct Go call. Its body has been read by the decoder; treat everything in it as client input. Set by Handler, OperationHandler and Begin, so every adapter has it.
WrapWriter(w, r, opts...) Returns a writer that applies r's effects just before the first header or byte, and a finish function that applies the effects of a handler that wrote nothing and reports how many effects came too late.
ApplyHeaderEffects(w, r, opts...) Writes every effect of r that can be a header (all kinds except a redirect) and returns how many effects it saw.
ApplyEffects(w, r, effects, opts...) Writes the given effects and reports whether one of them was a redirect (302 plus Location), in which case the response is complete.
BearerMode(r) Reports whether r asked for sessions as bearer tokens.
HasCredential(r, opts...) Reports whether r carries a session cookie or a bearer token at all, without looking it up, for a cheap redirect to the sign-in page before a page renders. Presence is not validity: a forged or expired cookie passes here, so protected actions still run behind Middleware and RequireUser.
LocaleOf(header) The first tag of an Accept-Language header, or "" when it is missing, *, longer than 35 characters (maxLocaleLen) or contains anything besides letters, digits and -.
Cookies
Constant Value Use
CookieName __Host-auth_session The session. __Host- makes the browser refuse it unless it is Secure, has Path=/ and has no Domain.
FlowCookieName __Host-auth_flow Binds a multi-step flow, such as an OAuth sign-in, to this browser.
DomainCookieName __Secure-auth_session The session under CookieDomain.
DomainFlowCookieName __Secure-auth_flow The flow cookie under CookieDomain.
CacheCookieName __Host-auth_session_cache The client's sealed copy of its session from a snapshot auth.SessionCache (auth.EffectCacheSession), so the next requests skip the session lookup. Its Max-Age is the cache TTL.
DomainCacheCookieName __Secure-auth_session_cache The cache cookie under CookieDomain.
DeviceCookieName __Host-auth_device The device token a second-factor plugin hands out with auth.EffectRememberDevice ("remember this browser"), presented back as auth.Request.Device. Set with the effect's expiry; an empty token deletes it. It survives sign-out, because it exists to skip the second factor on the next sign-in.
DomainDeviceCookieName __Secure-auth_device The device cookie under CookieDomain.
AccountCookiePrefix + 12 hex digits __Host-auth_account_… Under auth.WithMultiSession, one cookie per OTHER account signed in on the device (the active one stays in CookieName), named by the first 12 hex digits of the token's SHA-256 so one account can be dropped without rewriting the rest; the values are read into auth.Request.DeviceTokens. A sign-in moves the previously active token into its own account cookie with the new session's lifetime; a sign-in with a token the device already holds is a switch, which makes it active and deletes its account cookie; auth.EffectForgetDeviceSession deletes one; sign-out deletes them all. Automatic when the Auth has multi-session on, nothing to pass.
DomainAccountCookiePrefix __Secure-auth_account_… The account cookies under CookieDomain.
__Host-auth_<name> a plugin's A plugin cookie (auth.EffectCookie, recorded with c.Effects.SetCookie or c.SetSignedCookie), named by the plugin (auth.CheckCookieName: never one of the names above) and read back into auth.Request.Cookie. Same prefixes, same attributes; __Secure- under CookieDomain.

Every cookie is HttpOnly, Secure, SameSite=Lax and Path=/: SameSiteStrict and PartitionedCookies only add, and CookiePrefix and RenameCookie change only the name behind the browser prefix. Only the escape hatches WeakenCookies / WeakenCookie turn a protection off, and Handler and Middleware log WarnCookiesWeakened at start-up while one is set. A session issued without "remember me" is a browser-session cookie with no Expires; the server still ends it at its expiry. In host-only mode a __Secure- cookie is ignored, and under CookieDomain a __Host- one is, so a sibling subdomain cannot plant a session. Clearing the session (auth.EffectClearSession) also clears the cache cookie, and auth.EffectClearCache clears it alone. A cached copy recorded before either clear in the same response (a sign-out's own session lookup makes one) is not sent, as a cookie or as Set-Auth-Session-Cache; one recorded after it, by a sign-in that follows, is. A stateless session token (auth.WithStatelessSessions) is about 400 characters rather than 43; the cookie carries it whole and stays far under the 4 KB a browser accepts.

Bearer mode

A client that cannot keep cookies (a mobile app, a CLI) sends Auth-Token-Mode: bearer when it signs in, then Authorization: Bearer <token> on every request.

Constant Value Direction
TokenModeHeader Auth-Token-Mode request: ask for bearer mode
TokenModeBearer bearer the value of that header
SetTokenHeader Set-Auth-Token response: the issued session token
SetTokenExpiresHeader Set-Auth-Token-Expires response: its expiry, RFC 3339 UTC
SessionCacheHeader Auth-Session-Cache request: the cached session copy from an earlier response
SetSessionCacheHeader Set-Auth-Session-Cache response: a new cached copy to send back
DeviceHeader Auth-Device request: the device token of DeviceCookieName, when there is no cookie
SetDeviceHeader Set-Auth-Device response: a device token to keep (auth.EffectRememberDevice); an empty value means forget it

In bearer mode an issued session arrives only in Set-Auth-Token, never as a cookie and never in the JSON body, and a cached copy arrives in Set-Auth-Session-Cache. Signing out and clearing the cache set no header, because the client discards its own token and copy. When a request carries both a cookie and a bearer header, the cookie wins, and so does the cache cookie over Auth-Session-Cache. The Bearer scheme is compared case-insensitively, as RFC 7235 requires, and the token trimmed; Bearer alone or another scheme is no credential. A bearer client keeps its own set of accounts under multi-session, so auth.EffectForgetDeviceSession writes nothing to it.

Error JSON

Every error, from every route and helper, has this shape and carries the error's HTTP status:

{"error":{"code":"auth.signed_out","message":"Sign in to continue."}}

code is stable and meant for programs to branch on. message is safe to show a user. A message translated by auth.WithMessages (for example i18n.Messages()) adds originalMessage, the declared English text, as better-auth's i18n plugin does. A rate-limited answer adds Retry-After.

An app with its own error contract passes EncodeErrors(enc) to Handler, Middleware and every adapter; the body is then enc's, under the same status and Retry-After. WriteError(w, err) holds no request and always writes the shape above; call WriteErrorFor or WriteErrorWith from your own handlers to follow the encoder.

Tables and columns

None. The package reads and writes nothing itself; every row belongs to the core and the plugins.

Errors

Error Code Status When
ErrMethodNotAllowed auth.method_not_allowed 405 A known route called with the wrong method (Handler only).
ErrMissingScope auth.missing_scope 403 RequireScope on a principal (a key or a session) without the scope.
ErrNoUser (panic value, not JSON) none MustUser on a request with no user: a wiring bug.
auth.ErrUnknownOperation auth.unknown_operation 404 No route at this path.
auth.ErrBadRequest auth.bad_request 400 A body or query that does not decode into the operation's input.
auth.ErrOriginNotTrusted auth.origin_not_trusted 403 A cookie-authenticated mutation whose Origin (or, without one, Referer) is not trusted.
auth.ErrSignedOut auth.signed_out 401 RequireUser, or RequireScope, on a request with no principal.
auth.ErrInternal auth.internal 500 Anything that is not an *auth.Error, and a session lookup that failed. The detail is logged, never sent.

Security notes

Property Pinned by
The session cookie is __Host-, HttpOnly, Secure and SameSite=Lax, and the raw token never appears in a response body. TestHandler_IssueSessionSetsAHardenedCookie
A cookie-authenticated mutation from another origin is refused, using Referer when Origin is missing. TestHandler_OriginCheckOnCookieMutations
In bearer mode the token travels only in Set-Auth-Token. TestHandler_BearerModeReturnsTheTokenInAHeader, TestHTTP_BearerMode (integration)
Host-only mode ignores a __Secure- cookie that a sibling subdomain could have planted, and domain mode ignores the other name. TestCookieDomain_NamesDomainAndIgnoresTheOtherPrefix
Under CookiePrefix every cookie carries the prefix behind __Host-/__Secure-, only prefixed cookies are read, a prefix that is not a cookie-name token is refused when the option is built, and gin, echo, fiber, the gqlgen example and authui honour it. TestCookiePrefix_RenamesEveryCookieAndReadsOnlyThose, TestCookiePrefix_RefusesANonToken, TestMount_HonoursTheCookiePrefix (gin, echo, fiber), TestGraphQL_HonoursTheCookiePrefix, TestHTTPOptions_CookiePrefixIsTheCookieTheseLookUp
SameSiteStrict makes every cookie but the flow cookie Strict, deletions included, and PartitionedCookies adds the attribute without dropping HttpOnly or Secure; the defaults are unchanged. TestSameSiteStrict_StrictEverywhereButTheFlowCookie, TestPartitionedCookies_AddsTheAttributeAndKeepsTheRest
An internal error answers 500 with no detail, and the detail is logged. TestHandler_ErrorsUseTheirCodeAndStatus
A failed session lookup is a 500, never a silent sign-out. TestMiddleware_MissingDatabaseIsNotASignOut
An effect recorded after the response started is logged, not silently lost. TestMiddleware_LateEffectIsLogged
A rate-limited request answers 429 with Retry-After. TestHandler_RateLimitedAnswers429WithRetryAfter, TestWriteError_SetsRetryAfterForALimitedError
RequireScope answers 401 without a principal (no credential, a revoked key) and 403 for a key or session without the scope. TestRequireScope_NoPrincipalIsSignedOut, TestRequireScope_SeparatesNoPrincipalFromMissingScope (integration)
The cached session copy travels in a cookie as hardened as the session's, with Max-Age of the cache TTL, is never sent once expired, and is cleared with the session; in bearer mode it travels in the headers only. TestApplyEffects_CacheSessionCookie, TestApplyEffects_CacheSessionInBearerMode, TestRequestOf_ReadsTheCachedSession
With a snapshot cache the second request needs no database at all, and sign-out removes the copy. TestHTTP_SnapshotCacheSkipsTheLookup (integration)
A 400-character stateless token round-trips through the cookie under 4 KB. TestApplyEffects_ALongTokenFitsTheCookie, TestHTTP_StatelessSessionEndToEnd (integration)
A forwarded address is believed only from a trusted proxy, the rightmost untrusted entry wins, and text that is not an address is skipped; a bad network refuses to start. TestRequestOf_TrustedProxies, TestTrustedProxies_RefusesAValueThatIsNotANetwork
Forwarded is read only when named and only from a trusted proxy; the rightmost untrusted for= wins, a second header line counts as appended, and a client's unterminated quote cannot swallow the proxy's entry. TestForwarded_HonouredOnlyBehindTrustedProxies
ClientIPFunc decides the address, and an answer that is not an IP falls back to the connection and the proxy rules. TestClientIPFunc_OverridesAndFallsBack
Under EncodeErrors every error (operation, 404, 405, RequireUser behind Middleware, a failed Begin) is the encoder's body, the status is the library's whatever the encoder writes, Retry-After survives an encoder that removes it, and an internal error reaches the encoder without detail. TestEncodeErrors_ProblemJSONEverywhereStatusPinned, TestEncodeErrors_RateLimitKeepsRetryAfter, TestEncodeErrors_InternalErrorsStayOpaqueAndSilentEncoderStillAnswers, TestEncodeErrors_ReachesMiddlewareAndRequireUser (gin, echo, fiber)
A form body is refused unless AcceptForms is set, and then decodes to the same input as JSON. TestHandler_AcceptForms
A trailing slash is a 404 unless SkipTrailingSlash is set. TestHandler_SkipTrailingSlash
The locale cookie is read only when configured and only if it is a plausible tag. TestRequestOf_LocaleCookie
LocaleFunc decides first, its answer is reduced like a header, and an empty or unusable answer falls through to the cookie and the header. TestRequestOf_LocaleFunc
The reference page is absent unless Reference is passed, needs OpenAPI (start-up panic without it), names the served document under the prefix, loads the pinned or the named script, carries the per-request nonce attribute-escaped on both script tags, and answers only GET. TestReference_ServedOnlyWhenAsked
The health route is absent unless Health() is passed, answers only GET, and touches no database. TestHandler_HealthRouteIsOptIn, TestHTTP_HealthAndDenyByDefault (integration)
RequireUserExcept denies every path not listed, and an exact entry does not open its subtree. TestRequireUserExcept_DeniesByDefault, TestHTTP_HealthAndDenyByDefault (integration)
HasCredential reads the cookie name CookieDomain selects and never counts the other prefix. TestHasCredential_ReportsPresenceOnly
The body decoder survives arbitrary input. FuzzDecode (task fuzz)
ErrorPage escapes the description, shows unknown for a value not shaped like a code, takes the message from the library's declarations, never the link, and refuses an off-site Redirect. TestErrorPage_RendersTheCode, TestErrorPage_Options
CookieDomainFromBaseURL takes the domain from the request's checked base URL: a Host no rule allows gets the static base URL's domain. TestCookieDomainFromBaseURL
A plugin cookie gets the library's attributes whatever the plugin asked, under the same prefixes. TestPluginCookie_SetAndRead
Plugin middleware runs in plugin order, only on its paths, and is traced. TestPluginMiddleware

Limits:

  • A request body is capped at 1 MiB (maxBodyBytes).
  • The client IP is the connection's remote address. X-Forwarded-For and Forwarded are not trusted by default, because any client could otherwise pick its own IP and escape per-IP rate limits. An app behind a proxy names it with TrustedProxies, or decides itself with ClientIPFunc.

Example

Documentation

Overview

Package authhttp serves auth operations over net/http.

It is the reference transport and uses only the standard library, so it lives in the core module. Everything it does is available to any other transport through the same public API — Auth.Operations, Auth.WithRequest, Operation.Invoke, RequestEffects and ErrorOf — and it has no private path into the core. A router that accepts an http.Handler (chi, gorilla/mux, the standard mux) mounts it directly; gin and echo adapters reuse its request and effect handling.

Index

Constants

View Source
const (
	//ds:def id=domaincookiename-y5xjae8s owner=@khanakia stability=stable
	DomainCookieName = "__Secure-auth_session"
	//ds:def id=domainflowcookiename-pgbydmxs owner=@khanakia stability=stable
	DomainFlowCookieName  = "__Secure-auth_flow"
	DomainCacheCookieName = "__Secure-auth_session_cache"
	//ds:def id=domaindevicecookiename-3hn7kp9r owner=@khanakia stability=stable
	DomainDeviceCookieName    = "__Secure-auth_device"
	DomainAccountCookiePrefix = "__Secure-auth_account_"
)

Cookie names when CookieDomain is set. __Host- forbids a Domain attribute, so a cookie shared with subdomains uses the __Secure- prefix, which still requires HTTPS.

View Source
const (
	//ds:def id=tokenmodeheader-s6xp5wjc owner=@khanakia stability=stable
	TokenModeHeader = "Auth-Token-Mode"
	//ds:def id=tokenmodebearer-3p2r26xe owner=@khanakia stability=stable
	TokenModeBearer = "bearer"
	//ds:def id=settokenheader-j7vbxqvy owner=@khanakia stability=stable
	SetTokenHeader = "Set-Auth-Token"
	//ds:def id=settokenexpiresheader-rxwtm4ay owner=@khanakia stability=stable
	SetTokenExpiresHeader = "Set-Auth-Token-Expires"
	// SessionCacheHeader carries, in bearer mode, the client's copy of its
	// session from a snapshot SessionCache; SetSessionCacheHeader hands it
	// out. They are the bearer-mode twins of CacheCookieName.
	SessionCacheHeader    = "Auth-Session-Cache"
	SetSessionCacheHeader = "Set-Auth-Session-Cache"
	// DeviceHeader carries, in bearer mode, the device token of
	// DeviceCookieName; SetDeviceHeader hands it out, and an empty value
	// there means the client must forget it.
	//ds:def id=deviceheader-7rt2wq5m owner=@khanakia stability=stable
	DeviceHeader = "Auth-Device"
	//ds:def id=setdeviceheader-4kx8mv3p owner=@khanakia stability=stable
	SetDeviceHeader = "Set-Auth-Device"
)

Bearer mode. A client that cannot keep cookies — a mobile app, a CLI — sends TokenModeHeader: bearer on sign-in and then Authorization: Bearer on every request. An issued session then arrives in SetTokenHeader instead of a cookie, and never in the JSON body, so a cookie client's page scripts can never read a token out of a response. SetTokenHeader with an empty value, on any status, means the client must forget its token: the session ended (EffectClearSession) or the token sent names no session (ForgetRejectedToken).

View Source
const (
	ErrorPageTitleCode   = "authhttp.error_page.title"
	ErrorPageHeadingCode = "authhttp.error_page.heading"
	ErrorPageBackCode    = "authhttp.error_page.back"
)

Message codes of the built-in error page's fixed strings. They are not error codes: an app translates them in the same auth.WithMessages catalogue as its errors ({"de": {"authhttp.error_page.heading": "..."}}), and the page falls back to the English below for a locale that lacks one.

View Source
const (
	// TSFileClient is the generated part: one TypeScript type per Go input
	// and output type, one operation table and plugin factory per plugin,
	// and createAuthClient typed with the core's operations.
	TSFileClient = "client.ts"
	// TSFileRuntime is the hand-written fetch runtime, the same for every
	// app: request encoding, token modes, error decoding.
	TSFileRuntime = "runtime.ts"
	// TSFileErrorCodes is every error code the linked packages declare
	// (auth.Errors), the same module docs/reference/error-codes.ts is.
	TSFileErrorCodes = "error-codes.ts"
	// TSFileExtensions is the hand-written plugins that are not one
	// request each, the same for every app: oauthPopup() (the popup
	// sign-in of plugin/oauthproxy/oauthpopup) and lastLoginMethod().
	TSFileExtensions = "extensions.ts"
	// TSFileIndex re-exports the four, the module an app imports.
	TSFileIndex = "index.ts"
)

The files TypeScriptClient writes, by name. They import each other by these names, so a client is the five files in one directory.

View Source
const AccountCookiePrefix = "__Host-auth_account_"

AccountCookiePrefix starts the name of each cookie holding one of the device's OTHER signed-in accounts under auth.WithMultiSession, one cookie per session token so a single account can be dropped without rewriting the rest. The rest of the name is the first accountCookieHashLen hex digits of the token's SHA-256, which identifies the cookie to delete without the name itself being usable as a credential. The active account stays in CookieName.

View Source
const CacheCookieName = "__Host-auth_session_cache"

CacheCookieName holds the client's copy of its session from a snapshot SessionCache (auth.EffectCacheSession), so the next requests skip the session lookup. It is hardened like the session cookie: the value is sealed and tied to the session token, but it is still the user's record.

View Source
const CookieName = "__Host-auth_session"

CookieName is the session cookie. The __Host- prefix makes the browser refuse the cookie unless it is Secure, has Path=/ and has no Domain, so no subdomain can set or read it.

View Source
const DefaultCookiePrefix = "auth"

DefaultCookiePrefix is the app's part of every cookie name when CookiePrefix is not given: "auth" in "__Host-auth_session".

View Source
const DefaultLocaleCookieName = "auth_locale"

DefaultLocaleCookieName is the cookie LocaleCookie reads when given no name. It is not a __Host- cookie: a language choice is not a secret and an app's own page sets it from JavaScript.

View Source
const DefaultReferenceScript = "https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.72.4/dist/browser/standalone.js"

DefaultReferenceScript is the viewer the reference page loads when ReferenceOptions.ScriptURL is empty: Scalar's standalone bundle from jsDelivr, pinned to one release so the page does not change under an app that never asked it to. An app that will not load scripts from a third party serves the bundle itself and names its URL.

View Source
const DeviceCookieName = "__Host-auth_device"

DeviceCookieName holds the device token a second-factor plugin hands out with auth.EffectRememberDevice ("remember this browser"), presented back as auth.Request.Device. It is hardened like the session cookie and, unlike it, survives sign-out: it exists to skip the second factor on the NEXT sign-in, so clearing it with the session would defeat it.

View Source
const ErrorPagePath = "/error"

ErrorPagePath is the route ErrorPage adds under the prefix, better-auth's /api/auth/error: the page a failed browser flow lands on when it is given as auth.WithErrorURL (prefix + ErrorPagePath).

View Source
const FlowCookieName = "__Host-auth_flow"

FlowCookieName holds the binding of a multi-step flow such as an OAuth sign-in; see auth.EffectBindFlow.

View Source
const (

	// HeaderForwarded is the standard header (RFC 7239) a proxy writes the
	// client's address into, as for= parameters. It is not read by default;
	// name it in ClientIPHeaders to read it from a trusted proxy.
	HeaderForwarded = "Forwarded"
)

Headers a reverse proxy writes the client's address into. X-Forwarded-For is a list, nearest proxy last; X-Real-IP is one address.

View Source
const HealthPath = "/ok"

HealthPath is the route Health adds under the prefix. A load balancer or a deploy script calls it to learn that the auth routes are mounted and the process answers; it touches no database, so it says nothing about one.

View Source
const OpenAPIPath = "/openapi.json"

OpenAPIPath is the route OpenAPI adds under the prefix. The document is built once, when Handler is built, from the same operation list the routes come from, so it can never describe a route that is not served.

View Source
const OpenAPIVersion = "3.1.0"

OpenAPIVersion is the specification version the document declares. 3.1 is the first whose schemas are plain JSON Schema, which is what a nullable pointer field needs (`"type": ["string", "null"]`).

View Source
const ProtocolErrorMember = "auth_error"

ProtocolErrorMember is where a handler that answers a standard's own error body on an operation's route also puts the library's error ({code, message}), because the standard already owns "error": RFC 6749 §5.2's {"error": "authorization_pending"}, which deviceauth's CodeHandler and TokenHandler answer. A client of the standard ignores the extra member; the generated TypeScript client reads it, so it reports the plugin's declared code rather than an unreadable body. A handler that answers the library's own body (every handler in this package) never needs it.

View Source
const ReferencePath = "/reference"

ReferencePath is the route Reference adds under the prefix: a page that renders the OpenAPI document as an interactive API reference.

View Source
const WarnCookiesWeakened = "" /* 145-byte string literal not displayed */

WarnCookiesWeakened is logged by Handler and Middleware while WeakenCookies or WeakenCookie turns a protection off.

Variables

CookieRoleValues is every CookieRole.

View Source
var ErrMethodNotAllowed = auth.NewError("auth.method_not_allowed", http.StatusMethodNotAllowed, "This method is not allowed here.")

ErrMethodNotAllowed answers a known path called with the wrong method.

View Source
var ErrMissingScope = auth.NewError("auth.missing_scope", http.StatusForbidden, "This key does not allow this.")

ErrMissingScope answers a request that has a principal, a key or a session, without the scope a route requires.

View Source
var ErrNoUser = errors.New("authhttp: MustUser called on a request without a user; wrap the handler in RequireUser")

ErrNoUser is the panic value of MustUser outside RequireUser.

View Source
var ErrTypeScriptName = errors.New("authhttp: TypeScript name collision")

ErrTypeScriptName is returned by TypeScriptClient when two things would get one TypeScript name: two operations of a plugin whose ids differ only in punctuation, two Go types whose package and name concatenate alike, a plugin named after a TypeScript keyword or a runtime export. The message names both, so the plugin can be renamed.

ReferenceThemeValues is every ReferenceTheme, for validation and docs.

Functions

func ApplyEffects

func ApplyEffects(w http.ResponseWriter, r *http.Request, effects []auth.Effect, opts ...Option) (redirected bool)

ApplyEffects writes the effects of a request as HTTP headers and reports whether one of them was a redirect, in which case the response is complete. It must run before the body is written.

func ApplyHeaderEffects

func ApplyHeaderEffects(w http.ResponseWriter, r *http.Request, opts ...Option) int

ApplyHeaderEffects writes r's effects that can be headers — every kind but a redirect — and returns how many effects it saw. An adapter whose framework has its own writer type calls it just before the first write.

func BearerMode

func BearerMode(r *http.Request) bool

BearerMode reports whether r asked for sessions as bearer tokens.

func Begin

func Begin(a *auth.Auth, r *http.Request, opts ...Option) (*http.Request, error)

Begin attaches r to the core and resolves its session, returning the request with the new context. Every net/http-based adapter starts a request with it, so none of them builds the auth request differently.

func CurrentUser

func CurrentUser(r *http.Request) (auth.User, bool)

CurrentUser returns the signed-in user of r. It is auth.CurrentUser for code that holds a request rather than a context.

func ForgetRejectedToken

func ForgetRejectedToken(w http.ResponseWriter, r *http.Request)

ForgetRejectedToken tells a bearer client to forget its token when the request presented one that names no session (auth.RequestTokenRejected: unknown, expired, revoked, or older than the user's SessionsValidAfter), by sending SetTokenHeader with an empty value. A pending second-factor token is alive and is never cleared, which is why a client cannot decide this from an auth.signed_out answer alone. It leaves a SetTokenHeader already on w alone, so a session issued in the same response is never overwritten, and it does nothing in cookie mode, where a dead cookie costs nothing more than the 401. Every error and effect writer of this package calls it; an adapter that writes its own error body calls it before the first write. A nil r (Middleware answering a lookup that failed before a request existed) is no request to judge, and does nothing.

func HTTPRequest

func HTTPRequest(ctx context.Context) (*http.Request, bool)

HTTPRequest returns the *http.Request an operation, a hook or a handler behind Middleware is running for, when the call came through this package or an adapter built on it (Begin): better-auth's ctx.request, and with it ctx.query (r.URL.Query()). False for a direct Go call, which has no request.

It is the raw request on purpose, for what the typed input does not carry (a header the app's own proxy sets, a query parameter a hook keys on). Treat everything in it as client input, and do not read its body: the operation's decoder has already consumed it. c.Header and c.Client stay the transport-neutral way to read a header and the client's address.

func Handler

func Handler(a *auth.Auth, opts ...Option) http.Handler

Handler serves every registered operation: a Query as GET, a Mutation as POST, at prefix + the operation's route.

Routing is an exact-path table rather than http.ServeMux, because every route is a fixed path and the answers for a miss must be the library's own JSON errors: 404 for an unknown path, 405 with an Allow header for a known path and the wrong method. ServeMux cannot give both once a catch-all pattern exists.

func HasCredential

func HasCredential(r *http.Request, opts ...Option) bool

HasCredential reports whether r carries a session cookie or a bearer token at all, without looking it up. It is for a cheap outer layer, such as a redirect to the sign-in page before a page renders, where a database call per request is not wanted. Presence is not validity: a forged or expired cookie passes here, so every protected action still runs behind Middleware and RequireUser, which do the lookup.

func LocaleOf

func LocaleOf(header string) string

LocaleOf returns the first language tag of an Accept-Language header, or "" when it is missing or not a plausible tag. The value comes from the client, so it is reduced to letters, digits and "-" before anything uses it.

func MethodOf

func MethodOf(kind auth.OpKind) string

MethodOf is the HTTP method an operation of kind is served with. Exported so every net/http-based adapter maps kinds the same way.

func Middleware

func Middleware(a *auth.Auth, opts ...Option) func(http.Handler) http.Handler

Middleware attaches the request to the context and resolves the session, for the app's own handlers and for a GraphQL handler. Effects recorded by operations called inside the handler, such as a gqlgen sign-in mutation, are written as headers before the handler's first byte.

A session lookup that fails for a reason other than "signed out" — the database is down — answers 500 here rather than letting the handler run as if the user were signed out. A coded refusal from a token plugin (an API key that is disabled, expired or over its rate limit) answers its own code and status, as the gin, echo and fiber adapters do.

func MustUser

func MustUser(r *http.Request) auth.User

MustUser returns the signed-in user of r and panics without one. It is for handlers behind RequireUser, where a missing user is a wiring bug.

func OpenAPIDocument

func OpenAPIDocument(a *auth.Auth, info OpenAPIInfo, opts ...Option) []byte

OpenAPIDocument is the document OpenAPI serves, for a generator that writes it to a file and checks the committed copy (task docs:api). It takes the same options as Handler so the prefix, AcceptForms and Health in the document match the routes. It cannot fail: the document is nested maps of strings, numbers and booleans, which encoding/json always encodes.

func OperationHandler

func OperationHandler(a *auth.Auth, op auth.Operation, opts ...Option) http.Handler

OperationHandler serves one operation, for a router that registers each route itself, as the gin and echo adapters do. It answers the same way as Handler: same decoding, same effects, same error JSON.

func RequestOf

func RequestOf(r *http.Request, opts ...Option) auth.Request

RequestOf describes an http.Request to the core. Exported so gin, echo and any other net/http-based adapter build exactly the same Request.

The cookie wins over a bearer header when both are present. The client IP is the connection's remote address unless TrustedProxies names the network the request came from, because trusting X-Forwarded-For by default would let any client pick its own IP and escape per-IP rate limits.

func RequireFunc

func RequireFunc(check func(r *http.Request) error) func(http.Handler) http.Handler

RequireFunc refuses a request that check rejects, answering with check's error as JSON: an *auth.Error keeps its code and status, anything else is a 500 with no detail. It is the one place an app's own authorization rule — a role, the ownership of a resource — becomes middleware, so every such rule answers errors the same way. It must be inside Middleware.

func RequireScope

func RequireScope(scope string) func(http.Handler) http.Handler

RequireScope refuses a request unless it carries an API key with scope. A request with no principal at all (no credential, or a key that is revoked, expired or unknown) answers 401 auth.signed_out, so a client can tell "authenticate" from "you may not": a script whose key was revoked must get a new key, not ask for more scopes. A principal without the scope answers 403 auth.missing_scope, and that includes a browser session, because a route guarded by a scope is a route for keys. For a route both people and keys use, guard it with RequireUser and decide per request with auth.HasScope. It must be inside Middleware.

func RequireUser

func RequireUser(next http.Handler) http.Handler

RequireUser refuses a request without a session with 401 and the core's signed-out error, and one whose session has tasks pending with 403 auth.session_task_pending. It must be inside Middleware.

func RequireUserExcept

func RequireUserExcept(next http.Handler, allow ...string) http.Handler

RequireUserExcept is RequireUser for a whole handler tree with a list of paths that stay open: deny by default, allow by name, so a new page is protected until someone decides otherwise. An entry ending in "/" opens that subtree ("/auth/" opens every auth route); any other entry opens exactly that path ("/login"). It must be inside Middleware.

func SetRetryAfter

func SetRetryAfter(w http.ResponseWriter, err error)

SetRetryAfter sets the Retry-After header, in whole seconds, when err says when to retry (auth.RetryAfter), and does nothing otherwise. It must run before the status is written. It is exported for a transport that answers errors in its own format, such as authui's HTML pages, so every transport tells a limited client the same thing.

func TypeScriptClient

func TypeScriptClient(a *auth.Auth) (map[string][]byte, error)

TypeScriptClient writes a typed TypeScript client for every operation a serves, from the same operation list Handler and OpenAPIDocument read, so the client can never call a route that is not served or miss one that is. The answer maps a file name (TSFileClient, TSFileRuntime, TSFileErrorCodes, TSFileExtensions, TSFileIndex) to its contents; the five go in one directory.

Each plugin becomes a factory (password(), twofactor()) whose operation table carries the input and output types; createAuthClient({plugins}) types the client as the core's methods plus one namespace per plugin it was given, so calling a plugin that was not enabled is a compile error. Paths are the routes without a mount prefix: the client's baseURL carries it ("https://app.example.com/auth"), so one client serves any mount.

The types follow what encoding/json writes and the decoders read: a field without omitempty is required, a pointer, slice or map may be null, a `,string` field is a string, and a type with its own MarshalJSON is unknown. Run against an app's own *auth.Auth (authcli's `auth client ts`), it types that app's custom plugins and their error codes exactly.

func TypeScriptErrorCodes

func TypeScriptErrorCodes() []byte

TypeScriptErrorCodes is TSFileErrorCodes: a TypeScript module with every error code the linked packages declare (auth.Errors), sorted, as a const object of status, message and owning plugin, the union type of the codes, the wire shape of an error answer and a guard that narrows a string to a known code. The plugin is the code's prefix, or the core's name for the core's own codes (prefix "auth" or none). apiref writes the same bytes to docs/reference/error-codes.ts.

func WrapWriter

func WrapWriter(w http.ResponseWriter, r *http.Request, opts ...Option) (http.ResponseWriter, func() (late int))

WrapWriter returns a writer that applies r's effects as headers just before the first header or byte, and a finish function to call when the handler returns: it applies the effects of a handler that wrote nothing, and reports how many effects were recorded too late to become headers.

func WriteError

func WriteError(w http.ResponseWriter, err error)

WriteError answers err as the library's error JSON, with its status and, for a rate-limited error, its Retry-After header. An error that is not an *auth.Error answers 500 with no detail, so an adapter cannot leak one by accident. It holds no request, so it never applies EncodeErrors: an adapter that has the request calls WriteErrorFor or WriteErrorWith.

func WriteErrorFor

func WriteErrorFor(w http.ResponseWriter, r *http.Request, err error)

WriteErrorFor is WriteError with the message in the request's locale, when the app configured a catalogue (auth.WithMessages) and r went through Begin, Middleware or Handler so its context knows the Auth. Code and status are the same as WriteError's; without a catalogue so is the message. Every error the handlers and middleware of this package answer goes through it.

With EncodeErrors set on the Handler, Middleware or Begin r went through, the body is the encoder's, under the same status and Retry-After.

func WriteErrorWith

func WriteErrorWith(w http.ResponseWriter, r *http.Request, err error, opts ...Option)

WriteErrorWith is WriteError for an adapter that holds the request and the options but no context from Begin yet, as when Begin itself failed: the body is the EncodeErrors encoder's when opts (or r's context) set one, else the library's JSON. The message is not translated, as WriteError's is not, because a request whose session lookup failed was never attached to the core.

Types

type CookieAttributes

type CookieAttributes struct {
	// ScriptReadable drops HttpOnly: page script can read the cookie, so
	// one cross-site scripting bug hands the session to an attacker.
	ScriptReadable bool
	// Insecure drops Secure, and with it the __Host- or __Secure- prefix
	// (a browser refuses either without Secure), for plain-HTTP development
	// on a non-localhost host. The cookie then crosses the network in clear
	// text, and any subdomain or a network attacker can set it.
	Insecure bool
	// CrossSite sends the cookie with SameSite=None, so it travels on
	// requests from other sites, for an app embedded in a third-party page.
	// The origin check is then the only CSRF defence. It needs Secure, so
	// it cannot be combined with Insecure.
	CrossSite bool
	// Path narrows the cookie to a path other than "/". __Host- requires
	// Path=/, so the cookie is named with __Secure- instead, which a
	// sibling subdomain can set.
	Path string
}

CookieAttributes turns protections of a cookie off: better-auth's advanced.cookies.<cookie>.attributes and advanced.defaultCookieAttributes (with useSecureCookies). Every field removes one guarantee, so the zero value changes nothing, and Handler and Middleware log WarnCookiesWeakened at start-up while any is set.

type CookieRole

type CookieRole string

CookieRole names one of the cookies this package sets, for RenameCookie.

const (
	CookieSession CookieRole = suffixSession
	CookieFlow    CookieRole = suffixFlow
	CookieCache   CookieRole = suffixCache
	CookieDevice  CookieRole = suffixDevice
)

The cookies RenameCookie can rename. Their values are the role suffixes the default names end with. Account and plugin cookies keep the prefix rule, since each is a family of names rather than one.

type ErrorEncoder

type ErrorEncoder func(w http.ResponseWriter, r *http.Request, info auth.ErrorInfo)

ErrorEncoder writes an error response in an app's own shape, such as RFC 9457 problem+json or a house envelope, in place of {"error": info}. The library decides everything but the body: info is what auth.ErrorOf (or its translated form) made of the error, so a non-*auth.Error is already the 500 with no detail, and info.Status is the status the client gets.

The encoder cannot get the status wrong: whatever it passes to WriteHeader, the response carries info.Status, and an encoder that writes nothing still answers it with an empty body. Retry-After is set again after the encoder returns, so a limited client is told when to retry even if the encoder removed it. The encoder sets its own Content-Type.

func ErrorEncoderOf

func ErrorEncoderOf(ctx context.Context, opts ...Option) ErrorEncoder

ErrorEncoderOf is the EncodeErrors encoder opts set, else the one ctx carries from Begin, Middleware or Handler, else nil. An adapter whose framework cannot hand out an http.ResponseWriter (fiber) asks it to decide whether to record the encoder's answer or write the library's JSON itself.

type ErrorPageData

type ErrorPageData struct {
	Code        string
	Message     string
	Description string
	// Lang is the page's language tag: the request's locale when the
	// catalogue translates the page's heading for it, else "en", so the
	// lang attribute always names the language the fixed strings are in.
	Lang string
	// Title, Heading and Back are the page's fixed strings, from
	// auth.WithMessages under ErrorPageTitleCode, ErrorPageHeadingCode and
	// ErrorPageBackCode, else English.
	Title, Heading, Back string
	// Nonce is ErrorPageOptions.Nonce's value for this request, or empty.
	Nonce string
}

ErrorPageData is what the page template renders. Message is the code's declared message in the request's locale (auth.WithMessages) when the code is one the library declares, else empty. Description is the provider's error_description, untrusted text the template escapes like any other.

type ErrorPageOptions

type ErrorPageOptions struct {
	// Template replaces the built-in page, as better-auth's
	// customizeDefaultErrorPage restyles its own: any html/template that
	// renders ErrorPageData. html/template escapes every field, so a
	// template cannot be tricked into running a link's text.
	Template *template.Template
	// Redirect sends the request on to the app's own page instead of
	// rendering one, carrying the error and error_description parameters,
	// as better-auth's onAPIError.errorURL does at /api/auth/error. It must
	// be a same-site path or a URL on a trusted origin
	// (auth.TrustsReturnURL); Handler panics otherwise, at startup.
	Redirect string
	// Nonce returns a per-request CSP nonce for the built-in page's style
	// element, as ReferenceOptions.Nonce does for its scripts.
	Nonce func(*http.Request) string
}

ErrorPageOptions configures ErrorPage. Every field is optional.

type HTTPMiddlewarer

type HTTPMiddlewarer interface {
	auth.Plugin
	HTTPMiddleware() []PluginMiddleware
}

HTTPMiddlewarer is a plugin that adds net/http middleware to Handler. Handler applies every plugin's middleware in plugin order, the first plugin's outermost, and only to its own routes: the app's handlers are the app's to wrap. Each run is traced as an auth.SpanMiddleware span when auth.WithTracer is set.

It is a transport interface on purpose: an operation called from Go or GraphQL never passes through it, so a rule that must hold on every entry point belongs in an auth.OperationHook instead.

type OpenAPIInfo

type OpenAPIInfo struct {
	// Title and Version are required by the specification; empty values are
	// written as empty strings rather than invented.
	Title       string
	Version     string
	Description string
	// Servers are the base URLs the routes are reachable at, without the
	// prefix (the paths carry it). Empty means the document names none, and
	// a client uses the URL it fetched the document from.
	Servers []string
}

OpenAPIInfo is the part of the document the library cannot know: what the app is called, which version this is, and where it is served.

type Option

type Option func(*config)

Option configures Handler and Middleware.

func APIKeyFrom

func APIKeyFrom(get func(r *http.Request) string) Option

APIKeyFrom reads an API key from wherever the app's clients send it (a query parameter, a signed header pair), as better-auth's customAPIKeyGetter does; "" means the request has none. It runs only when no session cookie, no Authorization header and no APIKeyHeader value is present, and its value is handed to the core as a bearer credential exactly like those, so every key rule still applies. A key in a URL ends up in access logs and browser history; prefer a header. Off by default.

func APIKeyHeader

func APIKeyHeader(name string) Option

APIKeyHeader lets an API key arrive in the named header, such as "X-Api-Key", as well as in "Authorization: Bearer". The value is handed to the core as a bearer credential exactly like the Authorization form, so every rule that applies to a key there (no cookie, no origin check, scopes) applies here. A session cookie and an Authorization header both win over it. Off by default: an existing deployment accepts nothing new.

func AcceptForms

func AcceptForms() Option

AcceptForms lets a Mutation take an application/x-www-form-urlencoded body as well as JSON, one value per field, so a plain HTML form can post to a route and work without JavaScript. Nested inputs (lists, objects) are not expressible in a form and stay JSON-only. Off by default: a form body is then a bad request, as before.

func ClientIPFunc

func ClientIPFunc(fn func(r *http.Request) string) Option

ClientIPFunc replaces how the client's address is found, for a platform whose proxy rules TrustedProxies cannot express: the address arrives in a request context value, a signed header, or a list the app refreshes. fn owns the trust decision completely, so it must never return a value a client controls unless it has checked where the request came from. A value that is not an IP address ("" included) falls back to the connection's address and the TrustedProxies rules, so fn can decline a request and text cannot be smuggled into rate-limit keys or audit rows.

func ClientIPHeaders

func ClientIPHeaders(names ...string) Option

ClientIPHeaders replaces the headers a trusted proxy's client address is read from (default X-Forwarded-For, then X-Real-IP), for a proxy that writes another, such as CF-Connecting-IP or Fly-Client-IP. HeaderForwarded (RFC 7239) is read by its for= parameters, with the same rightmost rule as X-Forwarded-For. It has no effect without TrustedProxies: a Forwarded header from any other address is ignored like every other forwarded header, because a client can write it.

func CookieDomain

func CookieDomain(domain string) Option

CookieDomain shares the session with every subdomain of domain, such as "example.com" for app.example.com and admin.example.com. The cookies are then named DomainCookieName and DomainFlowCookieName.

Every subdomain can then read and set the session cookie, including one that serves user content or runs someone else's code. Use it only when every subdomain is yours and trusted; the default host-only cookie cannot be touched by any other host. Pass the same option to Handler, Middleware and every adapter, or they disagree about which cookie is the session.

func CookieDomainFromBaseURL

func CookieDomainFromBaseURL() Option

CookieDomainFromBaseURL is CookieDomain with the domain taken per request from the host of auth's BaseURL for that request, as better-auth's crossSubDomainCookies derives it from a dynamic base URL: a request to preview-42.example.com under auth.WithAllowedHosts sets cookies for preview-42.example.com and its subdomains. The host is one WithAllowedHosts accepted, or the static base URL's when it accepted none, so a forged Host header cannot choose the cookie's scope. The cookies are named like CookieDomain's (__Secure-), and the same warning applies: every subdomain of that host can read them. An explicit CookieDomain wins over it.

func CookiePrefix

func CookiePrefix(prefix string) Option

CookiePrefix replaces the "auth" in every cookie name: CookiePrefix("myapp") names the session cookie __Host-myapp_session (or __Secure-myapp_session under CookieDomain), and the flow, cache, device and account cookies the same way. Two apps on one host, or an app whose cookie policy names its cookies, use it. The browser prefix (__Host- or __Secure-) cannot be removed: it is what makes the browser enforce Secure, Path=/ and the domain rule. Pass the same option to Handler, Middleware and every adapter, as with CookieDomain, or they disagree about which cookie is the session.

It panics on an empty prefix or one that is not a cookie-name token (RFC 6265: no spaces, separators or control characters), because net/http would otherwise drop every Set-Cookie without a word and nobody could sign in.

func EncodeErrors

func EncodeErrors(enc ErrorEncoder) Option

EncodeErrors makes enc write every error body that Handler, Middleware, OperationHandler, RequireUser, RequireFunc and RequireScope answer, and the gin, echo and fiber adapters through them. The status, Retry-After and the decision of what a client may learn stay the library's (see ErrorEncoder). nil keeps the library's JSON.

func ErrorPage

func ErrorPage(opts ErrorPageOptions) Option

ErrorPage makes Handler serve GET prefix+ErrorPagePath: a page that shows a failed flow's error code, its message in the request's language and the provider's description, for auth.WithErrorURL to point at. Without it the route does not exist. The code is shown only when it is shaped like one, "unknown" otherwise, and the message comes from the library's own declarations, never from the link, so a crafted URL can put no text on the page but the escaped description.

func Health

func Health() Option

Health makes Handler answer GET prefix+HealthPath with {"ok":true}, for a liveness probe or a smoke test after a deploy. It is opt-in so an existing route table gains nothing it did not ask for, and it reads no database, so a green answer means "mounted and running", not "ready to sign people in".

func LocaleCookie

func LocaleCookie(name string) Option

LocaleCookie reads the request's locale from the named cookie (DefaultLocaleCookieName when name is "") before Accept-Language, so a language the user chose in the app wins over the browser's list. The value is reduced the way LocaleOf reduces a header; the app sets the cookie.

func LocaleFunc

func LocaleFunc(fn func(r *http.Request) string) Option

LocaleFunc lets the app decide the request's locale: fn runs first, and a non-empty answer (reduced like an Accept-Language value, so "fr-CA;q=0.9" is "fr-CA") wins over the cookie and the header. It is for a source the transport cannot know: the app's own cookie or header, a tenant's default, a preference it looks up by its own means. fn runs before the session is looked up, because the lookup's own error is rendered in the locale, so a preference stored on the user is reachable only through what the request itself carries.

func Logger

func Logger(l *slog.Logger) Option

Logger sets where internal errors are logged. Their text never reaches the client; without a logger they go to slog.Default().

func OpenAPI

func OpenAPI(info OpenAPIInfo) Option

OpenAPI makes Handler serve GET prefix+OpenAPIPath: the OpenAPI 3.1 document of every operation it serves, with input and output schemas derived from the operations' Go types. Opt-in, like Health, so an existing route table gains nothing it did not ask for; the document is public, because it describes routes a client can discover by calling them anyway and holds no deployment value.

func PartitionedCookies

func PartitionedCookies() Option

PartitionedCookies adds the Partitioned attribute (CHIPS) to every cookie, for an app embedded in another site's page: the browser then keeps one jar per top-level site, so a session established inside site A's frame is not sent from site B's. It only strengthens: the cookies stay Secure, HttpOnly and __Host- or __Secure-, which is what Partitioned requires.

func Prefix

func Prefix(p string) Option

Prefix sets the path the handler is mounted at, such as "/auth". It must match the pattern the handler is registered under.

func Reference

func Reference(opts ReferenceOptions) Option

Reference makes Handler serve GET prefix+ReferencePath: an HTML page that loads an OpenAPI viewer and points it at the document OpenAPI serves, so the routes an app wired can be browsed and tried from a browser. It needs OpenAPI, and Handler panics at start-up without it, because a page that renders a document nobody serves is a misconfiguration, not a runtime case. Opt-in, like OpenAPI: it is a development convenience, and it loads a script from a third party by default (DefaultReferenceScript), which an app decides for itself; the page itself holds nothing but two script tags.

func RenameCookie

func RenameCookie(role CookieRole, name string) Option

RenameCookie names one cookie, better-auth's advanced.cookies.<cookie>.name: RenameCookie(CookieSession, "sid") sets the session cookie as __Host-sid (__Secure-sid under CookieDomain), and the other cookies keep their prefix-built names. Only the name changes: the browser prefix stays, because it is what makes the browser enforce Secure, Path=/ and the domain rule, and the attributes (HttpOnly, Secure, SameSite) are the ones SameSiteStrict and PartitionedCookies set. Pass the same options to Handler, Middleware and every adapter, as with CookiePrefix.

It panics on an unknown role, or a name that is empty or not a cookie-name token, and Handler, Middleware and Begin panic when two cookies end up with one name, because each mistake would otherwise make the browser drop or overwrite a cookie and nobody could sign in.

func SameSiteStrict

func SameSiteStrict() Option

SameSiteStrict sends the session, cache, device and account cookies with SameSite=Strict instead of Lax: a top-level navigation from another site (a link in a mail, a redirect from a provider) then arrives with no session, so the landing page cannot act on it. The trade-off is after the OAuth return: browsers treat a redirect chain that started at the provider as cross-site, so the first page after the callback may render signed out; the app's landing page then navigates once more, or loads the session from script. The flow cookie stays Lax whatever this option says, because it exists for that very return and a Strict one would never arrive.

func SkipTrailingSlash

func SkipTrailingSlash() Option

SkipTrailingSlash makes Handler route "/auth/sign-out/" as "/auth/sign-out". Off by default: routes are exact, and a trailing slash is a 404.

func TrustedProxies

func TrustedProxies(networks ...string) Option

TrustedProxies names the networks whose X-Forwarded-For (then X-Real-IP, or the headers ClientIPHeaders sets) is believed, as CIDRs or single addresses ("10.0.0.0/8", "fd00::/8", "192.0.2.10"). A request from any other address keeps its connection address, so a client that invents the header gains nothing; a request from a trusted proxy takes the rightmost forwarded address that is not itself a trusted proxy, which is the one the nearest trusted hop appended. Without this option the client IP is always the connection's address, as before. A value that does not parse panics at start-up, because a deployment that believes the wrong network must not start.

func WeakenCookie

func WeakenCookie(role CookieRole, a CookieAttributes) Option

WeakenCookie is WeakenCookies for one cookie (session, flow, cache or device): better-auth's advanced.cookies.<cookie>.attributes. It replaces WeakenCookies' attributes for that cookie.

func WeakenCookies

func WeakenCookies(a CookieAttributes) Option

WeakenCookies turns protections off on every cookie this package sets: better-auth's defaultCookieAttributes. An escape hatch: off by default, warned about at every start (WarnCookiesWeakened), and each field's risk is in its doc. WeakenCookie overrides it for one cookie.

type PluginMiddleware

type PluginMiddleware struct {
	// Paths are the routes it runs on, after the Handler prefix: an exact
	// path ("/email/sign-in") or a prefix ending in "*" ("/email/*").
	// Empty means every request Handler receives, including one for an
	// unknown route.
	Paths []string
	Wrap  func(next http.Handler) http.Handler
}

PluginMiddleware is net/http middleware a plugin adds around Handler's routes, the Go form of a better-auth plugin's onRequest and onResponse (Paths empty: every request) and of its middlewares with a path matcher (Paths set). Wrap receives the next handler and may replace the request, answer early without calling next, or wrap the ResponseWriter to see the response, which is everything onRequest and onResponse can do.

type ReferenceOptions

type ReferenceOptions struct {
	// ScriptURL is the viewer script; empty means DefaultReferenceScript.
	ScriptURL string
	// Nonce, when set, is called per request and its answer becomes the
	// nonce attribute of both script tags, for an app whose
	// Content-Security-Policy allows scripts by nonce. The app's own
	// middleware sets the policy header; the page only carries the nonce.
	Nonce func(r *http.Request) string
	// Theme is the viewer's colour theme, better-auth's openAPI theme
	// option; empty keeps the viewer's own default. Reference panics at
	// start-up on a value outside ReferenceThemeValues, as it does for every
	// misconfiguration it can see.
	Theme ReferenceTheme
}

ReferenceOptions configures the page Reference serves.

type ReferenceTheme

type ReferenceTheme string

ReferenceTheme is one of the viewer's (Scalar's) named themes.

const (
	ThemeDefault    ReferenceTheme = "default"
	ThemeAlternate  ReferenceTheme = "alternate"
	ThemeMoon       ReferenceTheme = "moon"
	ThemePurple     ReferenceTheme = "purple"
	ThemeSolarized  ReferenceTheme = "solarized"
	ThemeBluePlanet ReferenceTheme = "bluePlanet"
	ThemeSaturn     ReferenceTheme = "saturn"
	ThemeKepler     ReferenceTheme = "kepler"
	ThemeMars       ReferenceTheme = "mars"
	ThemeDeepSpace  ReferenceTheme = "deepSpace"
	ThemeLaserwave  ReferenceTheme = "laserwave"
	// ThemeNone loads no theme stylesheet, for a page the app styles.
	ThemeNone ReferenceTheme = "none"
)

The themes the default viewer ships, the same set better-auth's openAPI plugin accepts.

type SignInMethod

type SignInMethod struct {
	// ID is unique among the methods, such as "oauth.github". A page keys
	// its translation on it ("authui.method." + ID) and a template may key
	// an icon on it.
	ID string
	// Kind groups methods for styling: "oauth", "sso", "passkey". A page
	// adds it as a class.
	Kind string
	// Label is the English button text, such as "Continue with GitHub".
	Label string
	// Route is the operation's route under the Handler prefix, such as
	// "/oauth/github/start" (auth.Operation.Route). It must start with one
	// "/": a page refuses anything else at start-up, so no method can
	// become a link to another site.
	Route string
	// Field, when set, makes the method a one-input GET form instead of a
	// link: the input's name, sent as a query parameter to Route, such as
	// "email" for an SSO start that finds the company by the address.
	Field string
	// FieldLabel is the English label of that input.
	FieldLabel string
}

SignInMethod is one way to sign in that a page can offer as a button or a link: "Continue with GitHub", "Sign in with your company". It names an operation route, not a URL, because only the app knows where it mounted Handler; a page joins the route to that prefix (authui's APIPrefix).

It is a browser navigation, a GET, on purpose: the route it names is a Query that answers with a redirect (an OAuth start, an SSO start), so a page offering it posts nothing and needs no origin check of its own. A method that needs JavaScript (a passkey ceremony) cannot be expressed as one and belongs in an app's own template.

func SignInMethods

func SignInMethods(a *auth.Auth) []SignInMethod

SignInMethods collects every UIMethod plugin's methods, in plugin order, for a page that renders its own sign-in screen.

type TSMethod

type TSMethod struct {
	// Namespace is where the method sits: "" for the core's methods on the
	// client's root, otherwise the plugin's factory and namespace name
	// (password() and client.password).
	Namespace string
	// Plugin is the plugin that registered the operation, as auth names it
	// (auth.CorePluginName for the core's).
	Plugin string
	// Name is the method's name: the operation id after the plugin's
	// prefix, in camelCase ("organization.invite-member" is inviteMember).
	Name string
	// ID is the operation it calls.
	ID auth.EndpointID
	// HTTPMethod and Path are the route, Path without a mount prefix.
	HTTPMethod, Path string
	// Require is what the call must carry before the operation runs.
	Require auth.Requirement
	// Input and Output are the TypeScript types of the method's argument and
	// of its result's data (EmptyInput when the operation takes no fields).
	Input, Output string
}

TSMethod is one method of the client TypeScriptClient writes, for a reference page or a tool that lists the client's surface without parsing TypeScript (apiref writes docs/reference/ts-client.md from it).

func TypeScriptMethods

func TypeScriptMethods(a *auth.Auth) ([]TSMethod, error)

TypeScriptMethods lists the methods of the client TypeScriptClient writes for a, sorted by namespace (the core's first) and then by name. It fails exactly when TypeScriptClient does, so a listed method always exists.

type UIMethod

type UIMethod interface {
	auth.Plugin
	SignInMethods() []SignInMethod
}

UIMethod is a plugin that offers sign-in methods to the pages an app renders, the ready-made authui pages included: an OAuth plugin lists one button per provider, an SSO plugin a "company sign-in" form. It is optional and read once, when the page handler is built.

It lives here rather than in authui so a plugin can implement it without importing authui, which is a module of its own.

Jump to

Keyboard shortcuts

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