oauthhttp

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: May 28, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package oauthhttp holds shared HTTP-response helpers used by the auth subpackages. Internal: only auth/* may import.

Index

Constants

View Source
const MaxErrorDescriptionRunes = 512

MaxErrorDescriptionRunes caps a sanitised server-supplied error_description by rune count. Real values are short ("user denied", "code expired", "audience denied"); past this the truncated form points at server misbehaviour rather than user-facing guidance, and unbounded length is a UX-DoS vector. Counted in runes rather than bytes so truncation lands on a valid UTF-8 boundary.

View Source
const MaxExpiresInSeconds = 100 * 365 * 24 * 60 * 60

MaxExpiresInSeconds caps a server-provided OAuth expires_in (seconds) before it is multiplied into a time.Duration. Without a ceiling, a value above ~9.2e9 overflows time.Duration's int64 nanosecond range, wrapping the derived expiry into the past so a freshly-issued token looks already-expired. 100 years is far beyond any real token lifetime and safely below the overflow threshold.

View Source
const MaxResponseBytes = 1 << 20

MaxResponseBytes caps how much of an OAuth response body we read. Both device-flow and token-exchange endpoints return small JSON documents; larger bodies indicate a misconfigured proxy or an attempt to exhaust client memory.

Variables

View Source
var ErrAbsolutePath = errors.New("path must be a relative URL, not absolute")

ErrAbsolutePath is returned by ResolveURL when the path is an absolute or scheme-relative URL rather than a path relative to BaseURL. Go's url.ResolveReference replaces BaseURL's host when handed an absolute reference, so accepting an absolute path would let any caller who can influence the configuration (env var, config file, server-discovery doc) redirect the request to an attacker — and capture whatever bearer the request carries.

View Source
var ErrInsecureBaseURL = errors.New("refusing to perform OAuth request over plain HTTP (set AllowInsecureHTTP only for local dev / test)")

ErrInsecureBaseURL is returned by ResolveURL when handed an http:// BaseURL without allowInsecureHTTP set. OAuth flows ship the user's bearer (subject_token, refresh_token, access_token) on the wire — over plain HTTP that's a credential in the clear. Public packages re-export this sentinel so callers can errors.Is on the familiar deviceflow.ErrInsecureBaseURL / sts.ErrInsecureBaseURL.

View Source
var ErrNonJSONResponse = errors.New(
	"could not reach authentication server: server returned non-JSON " +
		"response (check VPN, proxy, or firewall — e.g. Cloudflare WARP)",
)

ErrNonJSONResponse is returned by ReadAndDecodeJSON when a 200 OK from the authorization server's body looks like HTML rather than JSON — typically a captive portal, corporate proxy, or VPN firewall (Cloudflare WARP, etc.) intercepting the request and returning an error page.

Callers can match with errors.Is and surface a UX message; the default Error() string is already user-readable.

View Source
var ErrResponseTooLarge = errors.New("OAuth response body exceeds maximum size")

ErrResponseTooLarge is returned when an OAuth endpoint returns a response body larger than MaxResponseBytes. The helpers read one byte past the cap so callers get an explicit error instead of silently parsing a truncated response.

Functions

func ExpiresInDuration added in v0.4.0

func ExpiresInDuration(secs int) time.Duration

ExpiresInDuration converts a server-provided expires_in (seconds) into a time.Duration, clamping at MaxExpiresInSeconds to avoid int64 overflow. Callers still gate on expires_in > 0 where a non-positive value is invalid; this helper only defends the upper bound.

func HTTPClient added in v0.3.1

func HTTPClient(transport http.RoundTripper) *http.Client

HTTPClient builds the *http.Client used for one OAuth request.

Always returns a freshly-allocated *http.Client so this library's requests are isolated from mutations to http.DefaultClient (any process-wide Timeout or Transport swap by another package would otherwise be inherited). The underlying Transport is the caller-supplied transport when non-nil, or http.DefaultTransport otherwise — sharing a Transport is intentional (it owns the connection pool) and safe (Transport.RoundTrip is concurrent-safe).

Per-request timeouts must be driven by ctx.WithTimeout in the caller, not by *http.Client.Timeout — the body-read happens after client.Do returns, and Client.Timeout would cancel that read.

func IsLoopbackHost added in v0.3.4

func IsLoopbackHost(host string) bool

IsLoopbackHost reports whether host is one of the loopback hostnames this library permits for explicit insecure-http local development.

func NormalizeOriginURL added in v0.3.4

func NormalizeOriginURL(raw string) string

NormalizeOriginURL canonicalises an origin URL for equality comparisons. Scheme and host are lower-cased, default ports are stripped, and a trailing slash is collapsed away. On parse failure, or when raw is not an absolute URL, raw is returned unchanged so non-URL audience values still compare byte-for-byte.

func ReadAndDecodeJSON

func ReadAndDecodeJSON(r io.Reader, dest any, strict bool) error

ReadAndDecodeJSON reads up to MaxResponseBytes from r and decodes the body as JSON into dest. When strict is true, unknown fields are rejected.

Returns ErrNonJSONResponse when the body is HTML — the captive- portal / proxy-interceptor case. Other read or decode failures are wrapped with context.

func ResolveURL added in v0.3.1

func ResolveURL(baseURL, path string, allowInsecureHTTP bool) (string, error)

ResolveURL joins baseURL and path the way the OAuth flows want:

  • baseURL must parse, must have scheme http or https.
  • If scheme is http, allowInsecureHTTP must be true (otherwise ErrInsecureBaseURL).
  • path must parse and must be relative — neither absolute (e.g. "https://other") nor scheme-relative (e.g. "//other/path") references are accepted (otherwise ErrAbsolutePath). Without this guard, a caller controlling path could redirect the user's bearer to an attacker.

func SanitizeDescription added in v0.3.1

func SanitizeDescription(s string) string

SanitizeDescription strips control characters and caps length so a hostile or buggy AS can't write into the user's terminal or balloon CLI logs via the error_description field of an OAuth error response.

Rejected ranges:

  • C0 controls (U+0000–U+001F): ESC (0x1b) for ANSI sequences, CR, LF, TAB, NUL, BEL, etc.
  • DEL (U+007F).
  • C1 controls (U+0080–U+009F): notably CSI (U+009B), which is functionally equivalent to "ESC [" in 8-bit-aware terminals and can start an ANSI escape sequence on its own.

Preserves printable Unicode (including non-ASCII); truncates on rune boundaries rather than byte offsets so a CJK / emoji / combining-character payload can't be cut mid-rune into invalid UTF-8.

func ValidateClientID added in v0.4.0

func ValidateClientID(id string) error

ValidateClientID enforces the byte-level constraints RFC 6749 §2.3.1 and RFC 7617 §2 place on a client_id traveling via HTTP Basic Auth: VSCHAR (printable ASCII, 0x20–0x7E) and no ':' (the Basic Auth field separator). Without this guard, url.QueryEscape at the call site would percent-encode forbidden bytes, slip them past the wire validator, and surface as opaque server-side rejections after a QueryUnescape round-trip.

An empty client_id is permitted: public clients that don't authenticate at the token endpoint omit the credential entirely.

func ValidateClientIDConsistency added in v0.4.0

func ValidateClientIDConsistency(id string, extra url.Values) error

ValidateClientIDConsistency rejects requests that set client_id on both the typed field and Extra to different values, or that put more than one client_id in Extra. The two surfaces are populated independently — typed field becomes Basic Auth, Extra becomes form body — and a server reading one but not the other would silently accept the wrong identity. Same-value duplication is the documented belt-and-braces pattern.

Multiple client_id entries in Extra are always rejected, even when id is unset: servers that read via r.PostFormValue see only the first; servers that read via r.PostForm["client_id"] see all, so a slice like ["a","b"] succeeds against one and fails against the other in ways the caller can't predict.

func ValidateOriginURL added in v0.3.4

func ValidateOriginURL(raw string, allowInsecureHTTP bool, field string) (string, error)

ValidateOriginURL validates that raw is an origin URL. HTTPS is required unless allowInsecureHTTP is true and the host is loopback. The returned value is normalised with NormalizeOriginURL.

Types

type OAuthErrorResponse added in v0.3.4

type OAuthErrorResponse struct {
	Error            string `json:"error"`
	ErrorDescription string `json:"error_description"`
}

OAuthErrorResponse is the standard OAuth error response shape used by RFC 6749-family endpoints.

func ReadOAuthError added in v0.3.4

func ReadOAuthError(resp *http.Response) (*OAuthErrorResponse, error)

ReadOAuthError reads a non-success OAuth response body and returns the parsed OAuth error object when the server sent one. ErrorDescription on a returned OAuthErrorResponse is unsanitised; callers must pass it through SanitizeDescription before formatting it for logs or terminals. If the body is not an OAuth JSON error, the returned error contains a bounded, sanitised fallback message suitable for logs and terminals.

Return-shape contract: a non-nil *OAuthErrorResponse is returned only when its Error field is non-empty. Callers are safe to dereference apiErr.Error immediately after a nil-error check, but a future change that returns a partial apiErr would break that — keep the invariant when editing this function.

Jump to

Keyboard shortcuts

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