authithttp

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package authithttp is the only HTTP wiring authit's core ships: RFC-correct bearer-token extraction, token validation, the classification of what went wrong, refresh- and session-token cookies, and a cookie-session middleware with an optional CSRF check. It imports net/http and nothing else beyond authit's own jwt package — no router and no opinion about what a 401 body looks like. Those are the host application's business.

Everything here earns its place the same way: the step is identical in every consumer, and quietly security-critical when got slightly wrong.

Bearer tokens

The obvious hand-rolled version has several quiet failure modes:

  • strings.TrimPrefix(h, "Bearer ") returns the header unchanged when the prefix is absent, so a malformed header silently becomes a token string rather than a rejection.
  • The auth scheme is case-insensitive per RFC 7235, so a naive prefix check rejects a legitimate "bearer ..." header.
  • A missing header and an invalid token are both 401, but a signer that can't verify anything at all (a misconfigured key) is a 500 — and collapsing the two hides an outage behind a wall of 401s.

Typical use, inside whatever middleware the host already has:

claims, err := authithttp.Validate(signer, r)
if err != nil {
	w.WriteHeader(authithttp.StatusFor(err)) // 401 or 500
	return
}
// claims.Subject is the user ID; put it wherever your app keeps it.

Refresh cookies

The access token belongs in an Authorization header, which is what BearerToken reads. The refresh token does not: it is long-lived, so putting it anywhere JavaScript can reach means any XSS anywhere in the application yields a credential that outlives every access token. See SetRefreshCookie, ClearRefreshCookie and RefreshCookie, which take the decisions that are not really decisions — HttpOnly, Secure, SameSite — out of the caller's hands, and refuse to write a cookie the browser will silently discard.

A browser application on user.SessionModeOpaque keeps the one session token in a cookie. SetSessionCookie, ClearSessionCookie and SessionCookie handle it as the refresh-cookie helpers handle theirs. SessionMiddleware guards routes with it: claims into the context for ClaimsFrom, a 303 to the sign-in page for a browser navigation, 401 for everything else, 500 when the session store cannot answer, and the cookie's lifetime slid along with the session's. SafeReturnTo checks the return-to path the sign-in page is handed. CSRFProtect refuses state-changing requests from other origins, with a double-submit token as an option.

examples/sessions is all of it wired together over sqlitestore.

Index

Constants

View Source
const (
	DefaultCSRFCookieName = "authit_csrf"
	DefaultCSRFHeader     = "X-CSRF-Token"
	DefaultCSRFField      = "csrf_token"
)

DefaultCSRFCookieName, DefaultCSRFHeader and DefaultCSRFField are the defaults for the double-submit token in CSRFOptions.

View Source
const DefaultRefreshCookieName = "authit_refresh"

DefaultRefreshCookieName is the cookie name used when CookieOptions.Name is empty.

It carries no __Host- or __Secure- prefix, deliberately. Those prefixes are worth using and are supported here (see CookieOptions.Name), but switching a cookie's name invalidates every session already issued under the old one, so it is not something a default should do to an application that upgrades.

View Source
const DefaultReturnToParam = "next"

DefaultReturnToParam is the query parameter SessionMiddleware uses to tell the sign-in page where the user was going.

View Source
const DefaultSessionCookieName = "authit_session"

DefaultSessionCookieName is the cookie name used when SessionCookieOptions.Name is empty. Like DefaultRefreshCookieName it carries no prefix, for the same reason: renaming a cookie signs everybody out, which is not something a default should do on upgrade.

Variables

View Source
var (
	// ErrCookiePathRequired means CookieOptions.Path was empty.
	ErrCookiePathRequired = errors.New("authit/http: cookie Path is required")
	// ErrCookiePrefix means the cookie name carries a __Host- or __Secure-
	// prefix whose rules the other attributes break.
	ErrCookiePrefix = errors.New("authit/http: cookie name prefix conflicts with its attributes")
	// ErrCookieValue means the token cannot be stored in a cookie as-is.
	ErrCookieValue = errors.New("authit/http: token is not a valid cookie value")
)
View Source
var (
	// ErrCrossOrigin means a state-changing request came from another
	// origin, or did not say where it came from.
	ErrCrossOrigin = errors.New("authit/http: cross-origin request refused")
	// ErrCSRFToken means the double-submit token was missing or did not
	// match.
	ErrCSRFToken = errors.New("authit/http: missing or mismatched CSRF token")
)
View Source
var (
	// ErrNoToken means the request carried no usable bearer credential —
	// no Authorization header, a different scheme, or a malformed one.
	// The caller is unauthenticated: 401.
	ErrNoToken = errors.New("authit/http: no bearer token")
	// ErrInvalidToken means a credential was present but did not verify:
	// bad signature, expired, malformed, wrong issuer or audience. Errors
	// returned with this sentinel also wrap the signer's own error, so a
	// caller that wants to distinguish (say) expiry from forgery can still
	// check errors.Is(err, jwt.ErrTokenExpired). The caller is
	// unauthenticated: 401.
	//
	// Every plane's own ErrInvalidToken matches it too -- user, superuser,
	// pat and emaillogin -- so errors.Is(err, ErrInvalidToken) holds for an
	// error from user.Service.ValidateSession as much as for one from
	// Validate, and StatusFor needs no table of other packages' sentinels.
	ErrInvalidToken = sentinel.InvalidToken
)

Functions

func BearerToken

func BearerToken(r *http.Request) (string, bool)

BearerToken extracts the credential from the request's Authorization header per RFC 7235: the scheme is matched case-insensitively, and only a well-formed "Bearer <token>" yields ok.

It reports ok == false — never a garbage token — for a missing header, a different scheme, a scheme with no credential after it, or a credential containing whitespace. It also rejects a request carrying more than one Authorization header rather than picking one, since which of them the caller meant is not knowable here.

The returned token is not validated in any way; it is only syntactically a credential. Pass it to a Signer, or use Validate, which does both.

func CSRFProtect added in v0.7.0

func CSRFProtect(opts CSRFOptions) (func(http.Handler) http.Handler, error)

CSRFProtect refuses state-changing requests that another site could have made the browser send.

SameSite=Lax on the session cookie already keeps it off a cross-site POST. What it does not cover: a browser that ignores SameSite, a request from a sibling subdomain (same site, different origin), and a state-changing GET, which no middleware can save and no application should have.

Requests with a safe method (GET, HEAD, OPTIONS, TRACE) always pass. Every other request must come from the request's own host or one of TrustedOrigins, judged by Origin and, when the browser left that out, by Referer. The host is compared and not the scheme, as net/http.CrossOriginProtection does, so the check works unchanged behind a TLS-terminating proxy. With DoubleSubmit it must also echo the token.

It returns an error for options that cannot work: a token cookie the browser would discard, or a TrustedOrigins entry that is not an origin.

func CSRFToken added in v0.7.0

func CSRFToken(r *http.Request) string

CSRFToken returns the double-submit token for r, for rendering into a form or a <meta> tag. It is empty unless r passed through CSRFProtect with DoubleSubmit set.

func ClaimsFrom added in v0.7.0

func ClaimsFrom(ctx context.Context) (authitjwt.Claims, bool)

ClaimsFrom returns the claims SessionMiddleware put in the request's context. ok is false when there are none: the request did not pass through the middleware, or passed through it with Optional set and no valid session.

func ClearRefreshCookie added in v0.4.0

func ClearRefreshCookie(w http.ResponseWriter, opts CookieOptions) error

ClearRefreshCookie expires the cookie SetRefreshCookie wrote.

Pass the same CookieOptions used to set it. A browser matches a deletion against a cookie by name, domain and path, so clearing with a different Path leaves the original cookie in place -- the user appears to log out, the credential is still in the browser, and it still works. That is the mistake this function exists to make hard to write.

func ClearSessionCookie added in v0.7.0

func ClearSessionCookie(w http.ResponseWriter, r *http.Request, opts SessionCookieOptions) error

ClearSessionCookie expires the cookie SetSessionCookie wrote. Pass the same options: a deletion that does not match the cookie's name, domain and path leaves it in place, still working.

func ContextWithClaims added in v0.7.0

func ContextWithClaims(ctx context.Context, claims authitjwt.Claims) context.Context

ContextWithClaims returns a copy of ctx carrying claims, as SessionMiddleware stores them. It is exported for a host's own tests, and for a host that authenticates some other way and wants ClaimsFrom to keep working downstream.

func RefreshCookie added in v0.4.0

func RefreshCookie(r *http.Request, opts CookieOptions) (string, bool)

RefreshCookie reads the refresh token back off a request. It reports ok == false for a missing or empty cookie, and never a partial value.

The token is not validated here in any sense: it is an opaque string that only user.Service.Refresh can resolve.

func SafeReturnTo added in v0.7.0

func SafeReturnTo(target, fallback string) string

SafeReturnTo returns target if it is a path on this site, and fallback otherwise. Use it on the return-to value a sign-in page receives before redirecting to it, or the sign-in page becomes an open redirect: a link to your own domain that signs the user in and then delivers them, credentials fresh in mind, to a lookalike.

Only a path that starts with a single "/" passes. Rejected, among others: an absolute URL, a scheme-relative "//evil.example", the "/\evil.example" that browsers read as scheme-relative, anything with a control character in it (browsers strip tabs and newlines, turning "/\t/evil.example" into "//evil.example"), and "javascript:" in any spelling.

func SessionCookie added in v0.7.0

func SessionCookie(r *http.Request, opts SessionCookieOptions) (string, bool)

SessionCookie reads the session token back off a request. It reports ok == false for a missing or empty cookie. The token is not validated: it means something only to user.Service.ValidateSession.

func SessionMiddleware added in v0.7.0

func SessionMiddleware(v SessionValidator, opts SessionOptions) (func(http.Handler) http.Handler, error)

SessionMiddleware authenticates each request by the session cookie, validating it with v -- a *user.Service in SessionModeOpaque -- and puts the claims in the request's context, where ClaimsFrom finds them.

What it does with a request that has no valid session is the part every host used to write for itself:

  • The session could not be checked (the store is down, or v is a service in the wrong SessionMode): 500, never a redirect. A redirect would send a signed-in user to the sign-in page whenever the database hiccups, and they would sign in again, into the same outage.
  • No session, or an expired, revoked or unknown one: a browser navigation (GET or HEAD) is redirected with 303 to SignInURL, with the page it asked for in ReturnToParam. Every other request gets a 401. An invalid cookie is also cleared, so the browser stops sending it.
  • A valid session with Cookie.MaxAge set re-issues the cookie, so the browser keeps it for as long as the server-side session, which user.Service extends on use, keeps living. Its lifetime is capped at the session's own expiry.

It returns an error for options that cannot work: a cookie the browser would discard, or a SignInURL that does not parse.

func SetRefreshCookie added in v0.4.0

func SetRefreshCookie(w http.ResponseWriter, token string, opts CookieOptions) error

SetRefreshCookie writes the refresh token to the response as a cookie.

HttpOnly is always set and is not configurable. A refresh token readable from JavaScript is a refresh token stealable by any XSS anywhere in the application, and no use case justifies it -- the access token, which scripts do need, belongs in an Authorization header, which is what BearerToken reads.

It returns an error rather than writing a cookie the browser will discard: an empty Path, a token that is not a legal cookie value, or a name prefix whose rules the other attributes break.

func SetSessionCookie added in v0.7.0

func SetSessionCookie(w http.ResponseWriter, r *http.Request, token string, opts SessionCookieOptions) error

SetSessionCookie writes the session token to the response as a cookie.

It takes the request because, with AllowHTTP, whether the cookie is Secure depends on how the request arrived. Like SetRefreshCookie it returns an error rather than write a cookie the browser would discard.

func StatusFor

func StatusFor(err error) int

StatusFor maps an error from Validate, an Authenticator, or any plane's ErrInvalidToken to the status a bearer-authenticated endpoint should return: 401 when the request failed to authenticate itself, 500 when this server could not perform the check. It says nothing about the response body, and returns 200 for a nil error so it can be used unconditionally.

func Validate

func Validate(v authitjwt.Verifier, r *http.Request) (authitjwt.Claims, error)

Validate extracts the request's bearer token and verifies it with v.

The error it returns is classified, which is the point of the function. ErrNoToken and ErrInvalidToken both mean "this request is not authenticated" (401). Anything else means the verifier could not do its job at all — a key that isn't valid for its algorithm, or, for a Verifier that fetches keys, a fetch that failed — which is a server fault (500), not the caller's. Pass the error to StatusFor rather than assuming every failure here is a 401; collapsing the two hides a misconfigured deployment behind a wall of plausible-looking auth failures.

Validate deliberately does not decide anything beyond "is this token genuine and unexpired". Two things in particular are left to the caller:

  • Authorization. Claims.Subject is a user ID, not a permission. Whether that user may do this is the host's call, and a host that wants revocation to take effect before the token expires should re-resolve the principal from its own storage rather than trusting claims beyond the subject.
  • Impersonation. If an operator minted this token by impersonating the subject (see the superuser package), Claims.IsImpersonation reports true and Claims.ActorID names the operator. The token is genuine, so Validate accepts it; a route where acting-as-someone-else should not be allowed must check IsImpersonation itself.

It takes a Verifier rather than a Signer deliberately. Checking a token needs no ability to mint one, and with an asymmetric key the verifying half is safe to distribute where the signing half is not — see jwt.NewVerifier. A Signer satisfies Verifier, so passing one still works.

Types

type Authenticator added in v0.4.0

type Authenticator interface {
	Authenticate(ctx context.Context, r *http.Request) (authitjwt.Claims, error)
}

Authenticator turns a request into a verified principal.

It exists because "is this request authenticated" stopped being a pure function. Validate answers it by checking a signature and nothing else, which is what makes authit's default session model cheap: no lookup, no context, no way for the check to fail because a database is unreachable. A session validated by lookup — see user.Config.SessionMode — cannot be checked that way, and neither can a credential that lives somewhere authit has never heard of.

So this is the seam. Everything that guards a route takes one of these rather than a jwt.Verifier, and what is behind it is the host's choice:

  • VerifierAuth(v) is the default and does exactly what it always did.
  • SessionAuth(svc) looks an opaque session up on every request.
  • Your own implementation reads a gateway header, an mTLS certificate, or a cookie your own framework issued. authit does not need to know.

The error contract, which is the part worth getting right

Return ErrNoToken when the request carried no credential and ErrInvalidToken when it carried one that did not verify. Both mean 401. Anything else means the check could not be performed — the store is down, a key could not be fetched — and means 500, because answering 401 to that tells a caller their credential is bad when it may be fine, and hides an outage behind a wall of plausible auth failures.

StatusFor does that classification. An implementation that collapses its own failures into ErrInvalidToken has thrown the distinction away before StatusFor can see it.

func SessionAuth added in v0.4.0

func SessionAuth(v SessionValidator) Authenticator

SessionAuth validates an opaque session token by looking it up, which is what buys immediate revocation: a session revoked a moment ago is refused by the next request rather than by the one after the access token expires.

The cost is a round trip to the session store on every protected request. That is the trade, it is not hidden, and it is why this is not the default.

func VerifierAuth added in v0.4.0

func VerifierAuth(v authitjwt.Verifier) Authenticator

VerifierAuth is the default: verify the bearer token's signature, look nothing up.

The context is ignored, deliberately and permanently. Nothing here can block, so nothing here can be cancelled, and a host that wants revocation to take effect before a token expires wants SessionAuth or its own implementation instead.

type AuthenticatorFunc added in v0.4.0

type AuthenticatorFunc func(ctx context.Context, r *http.Request) (authitjwt.Claims, error)

AuthenticatorFunc adapts a function to Authenticator.

func (AuthenticatorFunc) Authenticate added in v0.4.0

func (f AuthenticatorFunc) Authenticate(ctx context.Context, r *http.Request) (authitjwt.Claims, error)

type CSRFOptions added in v0.7.0

type CSRFOptions struct {
	// TrustedOrigins are other origins allowed to send state-changing
	// requests, written as the browser sends them in Origin:
	// "https://admin.example.com", scheme and host, no path. The request's
	// own host is always allowed.
	TrustedOrigins []string

	// AllowMissingOrigin lets through a state-changing request that carries
	// neither Origin nor Referer. Browsers send Origin on every cross-origin
	// request and on same-origin POSTs, so such a request is almost always
	// a program -- curl, a test, a server-side client -- which is not what
	// CSRF is about. It is refused by default because "almost always" is
	// not "always": an old browser or a privacy extension can strip both.
	AllowMissingOrigin bool

	// DoubleSubmit additionally requires a token: a random value set in a
	// cookie, which the page must echo back in the TokenHeader header or
	// the TokenField form field. Read it for rendering with CSRFToken.
	// The cookie is HttpOnly, so a script takes it from the page (a <meta>
	// tag, say), not from document.cookie.
	DoubleSubmit bool

	// TokenCookie is the double-submit cookie. Its Name defaults to
	// DefaultCSRFCookieName and its SameSite to Strict; everything else,
	// including AllowHTTP, works as for the session cookie. A __Host-
	// name is worth its constraints here: without it, a sibling subdomain
	// can plant a cookie of its own and so choose the token.
	TokenCookie SessionCookieOptions

	// TokenHeader and TokenField default to DefaultCSRFHeader and
	// DefaultCSRFField. The header is checked first; the form field is
	// read only when the header is absent, and reading it parses the body.
	TokenHeader string
	TokenField  string

	// ErrorHandler writes the response for a refused request. err is
	// ErrCrossOrigin or ErrCSRFToken. The default is a 403 with the status
	// text.
	ErrorHandler func(w http.ResponseWriter, r *http.Request, err error)
}

CSRFOptions configures CSRFProtect.

type CookieOptions added in v0.4.0

type CookieOptions struct {
	// Name defaults to DefaultRefreshCookieName.
	//
	// Two prefixes are worth knowing about, and both are validated here
	// rather than left to fail silently in the browser:
	//
	//   __Secure-  requires Secure. Compatible with a scoped Path, so it
	//              is the one to reach for.
	//   __Host-    requires Secure, Path="/" and no Domain. Strongest --
	//              it cannot be set or overwritten by a subdomain -- but
	//              incompatible with scoping the cookie to a refresh
	//              route, so it is a real trade rather than a free win.
	//
	// A browser silently ignores a Set-Cookie whose prefix rules are
	// broken: no error, no cookie, and a login that appears to succeed and
	// then cannot refresh. Set/Clear return an error instead.
	Name string

	// Path is required. It is the point of the exercise: scoped to your
	// refresh route ("/auth/refresh"), the cookie is attached to that one
	// request instead of every request the browser makes to your origin,
	// so a long-lived credential stops being sprayed across your entire
	// API surface.
	//
	// Use "/" only if you have decided you want that.
	Path string

	// Domain is empty by default, which makes the cookie host-only: sent
	// to exactly the host that set it, and never to a sibling subdomain.
	// Setting it widens that to a domain and everything under it, which is
	// worth doing only if you actually need to.
	Domain string

	// MaxAge is how long the browser keeps the cookie. Zero makes it a
	// session cookie, discarded when the browser closes -- the safer
	// default, and the wrong one if you want people to stay signed in.
	// Set it to the same value as user.Config.RefreshTokenTTL when you do.
	MaxAge time.Duration

	// SameSite defaults to http.SameSiteStrictMode. A refresh cookie is
	// only ever sent by your own application's own request to its own
	// refresh route, so Strict costs nothing and removes the cookie from
	// cross-site requests entirely.
	SameSite http.SameSite

	// Insecure drops the Secure attribute, allowing the cookie over plain
	// HTTP. The field is spelled this way round so the unsafe choice is
	// the one you have to type: it exists for local development against
	// http://localhost and has no other legitimate use.
	Insecure bool
}

CookieOptions describes how a refresh token is stored in the browser.

Why this is here at all

authit is a service layer and ships no cookie handling as a rule. The refresh token is the exception for the same reason bearer parsing is: it is a long-lived credential, the correct handling is identical in every consumer, and the incorrect handling is both easy and common. Left to "the caller's business", a refresh token ends up in localStorage, where any XSS on any page of the application reads it and gets a credential that outlives every access token.

type SessionCookieOptions added in v0.7.0

type SessionCookieOptions struct {
	// Name defaults to DefaultSessionCookieName. A __Host- or __Secure-
	// prefix is validated as it is for CookieOptions.Name, and cannot be
	// combined with AllowHTTP: both prefixes require Secure on every
	// response, which AllowHTTP exists to drop.
	Name string

	// Path defaults to "/".
	Path string

	// Domain is empty by default, which makes the cookie host-only.
	Domain string

	// MaxAge is how long the browser keeps the cookie. Zero makes it a
	// browser-session cookie, discarded when the browser closes. To keep
	// people signed in, set it to user.Config.RefreshTokenTTL, which is
	// the session's lifetime in opaque mode; SessionMiddleware then slides
	// the cookie along with the session.
	MaxAge time.Duration

	// SameSite defaults to http.SameSiteLaxMode.
	SameSite http.SameSite

	// AllowHTTP sets Secure only on a response to a request that arrived
	// over HTTPS, instead of on every response. Without it -- the default
	// -- the cookie is always Secure, which is correct behind a
	// TLS-terminating proxy as it stands: the browser is talking HTTPS
	// even though this server is not. It exists for development against
	// http://localhost.
	AllowHTTP bool

	// TrustForwardedProto makes AllowHTTP treat a request whose
	// X-Forwarded-Proto is "https" as HTTPS. Set it only behind a proxy
	// that sets that header itself and overwrites whatever the client
	// sent; otherwise a client chooses the answer. It has no effect
	// without AllowHTTP, since the cookie is then Secure regardless.
	TrustForwardedProto bool
}

SessionCookieOptions describes how a session token -- the single credential user.SessionModeOpaque issues -- is stored in the browser.

It mirrors CookieOptions, and differs from it where a session cookie is a different thing from a refresh cookie:

  • Path defaults to "/". A refresh cookie is scoped to the one route that spends it; a session cookie is the credential every page needs.
  • SameSite defaults to Lax, not Strict. Under Strict, a signed-in user who follows a link to the application from an email or a chat arrives without the cookie and is shown the sign-in page. Lax still withholds it from cross-site POSTs, which is the request CSRF is made of; CSRFProtect covers what Lax leaves.
  • Secure is decided per request when AllowHTTP is set, so one configuration can serve http://localhost in development and HTTPS behind a TLS-terminating proxy in production.

HttpOnly is always set, for the reason given on SetRefreshCookie.

type SessionOptions added in v0.7.0

type SessionOptions struct {
	// Cookie is where the session token is read from. Use the same options
	// for SetSessionCookie at sign-in, or the middleware looks for a
	// cookie that is not there.
	Cookie SessionCookieOptions

	// SignInURL is where a browser is sent when it asks for a page without
	// a valid session. Empty means never redirect: every unauthenticated
	// request is a 401. Do not wrap the sign-in page itself in the
	// middleware, or the redirect loops.
	//
	// Only a GET or HEAD that IsAPIRequest does not claim is redirected.
	// Anything else gets a 401: redirecting a POST loses its body, and an
	// API client wants a status it can act on, not a sign-in form.
	SignInURL string

	// ReturnToParam is the query parameter added to SignInURL carrying the
	// path and query the user asked for. Defaults to DefaultReturnToParam.
	// Pass the value through SafeReturnTo before redirecting to it after
	// sign-in; it arrives from the browser, so anyone can put anything in
	// it.
	ReturnToParam string

	// IsAPIRequest reports whether a request is from a program rather than
	// a person navigating, and so wants a 401 rather than a redirect. The
	// default treats a request as a browser navigation when Sec-Fetch-Mode
	// is "navigate", or, when that header is absent, when Accept mentions
	// text/html; everything else is an API request.
	IsAPIRequest func(*http.Request) bool

	// Optional lets a request with no valid session through to the next
	// handler, without claims, instead of refusing it. It is for pages
	// that render differently when signed in -- a home page that shows
	// either "sign in" or the user's name. A failing session store is
	// still a 500: "could not check" is not "not signed in".
	Optional bool

	// ErrorHandler writes the response for a refused request: 401, or 500
	// when the session could not be checked. The default writes the status
	// text. err is what the validator returned, or ErrNoToken.
	ErrorHandler func(w http.ResponseWriter, r *http.Request, status int, err error)
}

SessionOptions configures SessionMiddleware. The zero value is usable: a cookie named DefaultSessionCookieName, and 401 for every unauthenticated request.

type SessionValidator added in v0.4.0

type SessionValidator interface {
	ValidateSession(ctx context.Context, token string) (authitjwt.Claims, error)
}

SessionValidator resolves an opaque session token to its principal. It is what *user.Service satisfies, expressed as an interface so this package does not import the service packages it is meant to sit underneath.

Return an error wrapping ErrInvalidToken for a session that is revoked, expired or unknown, and the store's own error for a lookup that failed.

Jump to

Keyboard shortcuts

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