authithttp

package
v0.6.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: 8 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, and refresh-token cookies. It imports net/http and nothing else beyond authit's own jwt package — no router, no http.Handler, no context key, 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.

Index

Constants

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.

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 (
	// 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.
	ErrInvalidToken = errors.New("authit/http: invalid bearer token")
)

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 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 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 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 StatusFor

func StatusFor(err error) int

StatusFor maps an error from Validate 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 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 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