bearer

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jun 20, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

README

go-bearer-token

typed extraction of OAuth 2.0 bearer tokens from HTTP requests, and composition of the WWW-Authenticate: Bearer challenge response.

Implements RFC 6750 — The OAuth 2.0 Authorization Framework: Bearer Token Usage (Proposed Standard, 2012-10). Spec: https://www.rfc-editor.org/rfc/rfc6750.html

What this library is and is not

A resource server speaks RFC 6750 at two moments: when a request arrives (pull the bearer token out of it, §2) and when it is refused (tell the client how, via a WWW-Authenticate: Bearer challenge with a typed error code, §3). This library owns exactly those two halves — the wire shape of bearer-token transport and the challenge response. Zero non-test dependencies: standard library only.

It does not:

  • Validate the token. Verifying a JWT access token is go-access-tokens (RFC 9068); validating an opaque token is go-token-introspection (RFC 7662). This library is upstream of both: it extracts the credential string and hands it on.
  • Make the authorization decision (are the scopes satisfied? who is the subject?).
  • Terminate TLS. RFC 6750 §1 requires it, but that is transport configuration, not this library.

Status

Pre-v0.1.0. The public API is not yet stable.

Install

go get github.com/hstern/go-bearer-token

Quickstart

Extract a token (§2)

The Authorization: Bearer header (§2.1) is read by default. The other two transport methods the spec defines are discouraged, so they are opt-in:

import bearer "github.com/hstern/go-bearer-token"

token, err := bearer.Token(r)                       // §2.1 header only
token, err := bearer.Token(r, bearer.WithFormBody()) // also §2.2 form body
token, err := bearer.Token(r, bearer.WithURIQuery()) // also §2.3 query string

Token enforces the §2 rules for you: the form-body method is never honored on GET, a token presented in more than one enabled location is rejected, and a present-but-malformed credential is reported distinctly from an absent one:

token, err := bearer.Token(r)
switch {
case errors.Is(err, bearer.ErrNoToken):
	// No credentials — answer with a bare challenge and 401.
	bearer.Challenge{Realm: "example"}.Respond(w)
	return
case err != nil:
	// errors.Is(err, bearer.ErrMultipleTokens) or ErrMalformedToken —
	// both are an invalid_request (400).
	bearer.Challenge{Realm: "example", Error: bearer.ErrorInvalidRequest,
		ErrorDescription: err.Error()}.Respond(w)
	return
}

// ... validate token (signature, claims, scope) with another library ...

The §2.3 query-string method leaks tokens into logs, browser history, and Referer headers; the spec marks it SHOULD NOT. A server that enables it with WithURIQuery MUST send Cache-Control: no-store on those responses (§2.3) — this library does not set that header for you.

Compose a challenge (§3)

Challenge is the typed WWW-Authenticate: Bearer value. The zero value is a valid bare challenge (Bearer); set Error to one of the §3.1 codes to make it an error challenge.

c := bearer.Challenge{
	Realm:            "example",
	Error:            bearer.ErrorInvalidToken,
	ErrorDescription: "The access token expired",
}

c.String()       // Bearer realm="example", error="invalid_token", error_description="The access token expired"
c.SetHeader(w)   // sets only the WWW-Authenticate header — you write the status/body
c.Respond(w)     // sets the header and writes the status (here, 401); no body

Use Respond for the common bodyless challenge; use SetHeader when the response also carries a body (for example a JSON error document), so the challenge composes with your own status and body.

The three error codes carry their canonical HTTP status (§3.1):

Code Status Meaning
bearer.ErrorInvalidRequest (400) 400 malformed request / more than one method
bearer.ErrorInvalidToken (401) 401 token expired, revoked, or invalid
bearer.ErrorInsufficientScope (403) 403 token lacks the required scope

bearer.StatusFor(code) returns the status for a code; an empty or unknown code (the bare-challenge case) returns 401.

Extension parameters

Challenge.Extra carries auth-params beyond the ones RFC 6750 names — for example the RFC 9728 resource_metadata parameter — rendered after the named fields in sorted key order:

c := bearer.Challenge{
	Error: bearer.ErrorInvalidToken,
	Extra: map[string]string{"resource_metadata": "https://rs.example.com/.well-known/oauth-protected-resource"},
}

License

Apache-2.0 — see LICENSE.

Documentation

Overview

Package bearer implements RFC 6750 — the OAuth 2.0 Bearer Token Usage profile: extracting a bearer token from an HTTP request (§2) and composing the WWW-Authenticate: Bearer challenge a resource server returns when it refuses one (§3).

It owns exactly two things: the wire shape of bearer-token transport and the challenge response. It does not validate the token (that is the JWT profile, RFC 9068, or introspection, RFC 7662) and it does not make the authorization decision. The intended resource-server flow is:

  1. extract the token from the request with Token;
  2. validate it (signature, claims, scope) with the appropriate library;
  3. on failure, answer with a Challenge carrying the right §3.1 error code.

Extraction

Token reads the Authorization: Bearer header (§2.1) by default. The other two transport methods are opt-in because the spec discourages them: the form-encoded body parameter (§2.2) via WithFormBody, and the URI query parameter (§2.3) — which the spec marks SHOULD NOT — via WithURIQuery. A request presenting a token in more than one enabled location is rejected with ErrMultipleTokens: the §2 "more than one method" invalid_request.

Challenge

Challenge is the typed WWW-Authenticate: Bearer value. Build one, then render it with Challenge.String, set just the header with Challenge.SetHeader, or write a complete bodyless response (header plus status) with Challenge.Respond. StatusFor maps a §3.1 error code to its canonical HTTP status.

Spec: https://www.rfc-editor.org/rfc/rfc6750.html

Example (InsufficientScope)

Example_insufficientScope advertises the scopes that would satisfy the resource (§3), paired with a 403.

package main

import (
	"fmt"

	bearer "github.com/hstern/go-bearer-token"
)

func main() {
	c := bearer.Challenge{
		Error: bearer.ErrorInsufficientScope,
		Scope: "read write",
	}
	fmt.Println(c)
	fmt.Println("status:", bearer.StatusFor(c.Error))
}
Output:
Bearer error="insufficient_scope", scope="read write"
status: 403
Example (InvalidToken)

Example_invalidToken builds the canonical §3 challenge for a token that failed validation.

package main

import (
	"fmt"

	bearer "github.com/hstern/go-bearer-token"
)

func main() {
	c := bearer.Challenge{
		Realm:            "example",
		Error:            bearer.ErrorInvalidToken,
		ErrorDescription: "The access token expired",
	}
	fmt.Println(c)
	fmt.Println("status:", bearer.StatusFor(c.Error))
}
Output:
Bearer realm="example", error="invalid_token", error_description="The access token expired"
status: 401
Example (ResourceServer)

Example_resourceServer shows the two halves of RFC 6750 a resource server uses: extract the token on the way in, and answer with a typed challenge when it has to refuse. Token validation (the middle step) belongs to another library and is elided here.

package main

import (
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"

	bearer "github.com/hstern/go-bearer-token"
)

func main() {
	handler := func(w http.ResponseWriter, r *http.Request) {
		token, err := bearer.Token(r)
		switch {
		case errors.Is(err, bearer.ErrNoToken):
			// No credentials: a bare challenge with 401 (§3).
			bearer.Challenge{Realm: "example"}.Respond(w)
			return
		case err != nil:
			// Malformed or multiply-presented token: invalid_request / 400.
			bearer.Challenge{
				Realm:            "example",
				Error:            bearer.ErrorInvalidRequest,
				ErrorDescription: err.Error(),
			}.Respond(w)
			return
		}

		// ... validate token here (signature, claims, scope) ...
		_ = token
		_, _ = fmt.Fprintln(w, "ok")
	}

	// A request with no Authorization header gets the bare challenge.
	w := httptest.NewRecorder()
	handler(w, httptest.NewRequest(http.MethodGet, "/", nil))

	fmt.Println("status:", w.Code)
	fmt.Println("challenge:", w.Header().Get("WWW-Authenticate"))
}
Output:
status: 401
challenge: Bearer realm="example"

Index

Examples

Constants

View Source
const (
	// ErrorInvalidRequest (HTTP 400) — the request is malformed: a missing or
	// repeated parameter, or a token presented by more than one method (§2).
	ErrorInvalidRequest = "invalid_request"
	// ErrorInvalidToken (HTTP 401) — the access token is expired, revoked,
	// malformed, or otherwise invalid.
	ErrorInvalidToken = "invalid_token"
	// ErrorInsufficientScope (HTTP 403) — the token lacks the scope the
	// resource requires.
	ErrorInsufficientScope = "insufficient_scope"
)

Error codes for the WWW-Authenticate: Bearer challenge (§3.1). Each has a canonical HTTP status; see StatusFor.

View Source
const SpecVersion = "RFC 6750"

SpecVersion is the version of the specification this package targets.

Variables

View Source
var (
	// ErrNoToken means no bearer token was present in any enabled location.
	// A resource server answers this with a bare WWW-Authenticate: Bearer
	// challenge and HTTP 401 (§3) — the request simply carried no credentials,
	// which is not itself a protocol error.
	ErrNoToken = errors.New("bearer: no bearer token in request")

	// ErrMultipleTokens means a token was presented in more than one place —
	// across methods, or repeated within one. This is the §2 "more than one
	// method" rule, an invalid_request (HTTP 400).
	ErrMultipleTokens = errors.New("bearer: token presented in multiple places")

	// ErrMalformedToken means a bearer token was present but does not match the
	// §2.1 b64token syntax (for example it contains a space or control byte),
	// or the Authorization: Bearer header carried no token at all. This is an
	// invalid_request (HTTP 400).
	ErrMalformedToken = errors.New("bearer: malformed bearer credentials")
)

Errors returned by Token.

Functions

func StatusFor

func StatusFor(errCode string) int

StatusFor returns the HTTP status code that accompanies a §3.1 error code: invalid_request → 400, invalid_token → 401, insufficient_scope → 403. The empty string (a bare challenge, sent with a 401 when a request carries no credentials) and any unrecognized code return 401.

func Token

func Token(r *http.Request, opts ...ExtractOption) (string, error)

Token extracts the bearer token from an HTTP request per RFC 6750 §2. It reads the Authorization: Bearer header by default; pass WithFormBody and/or WithURIQuery to also accept the body and query-string methods.

It returns ErrNoToken when no token is present, ErrMultipleTokens when a token appears in more than one enabled location (the §2 invalid_request), and ErrMalformedToken when a present token violates the §2.1 b64token syntax. Match these with errors.Is to choose the right Challenge. The returned token is the raw credential string; this package does not validate it.

Example (Query)

ExampleToken_query enables the discouraged §2.3 query-parameter method. A server that does this MUST also send Cache-Control: no-store.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	bearer "github.com/hstern/go-bearer-token"
)

func main() {
	r := httptest.NewRequest(http.MethodGet, "/resource?access_token=mF_9.B5f-4.1JqM", nil)

	token, err := bearer.Token(r, bearer.WithURIQuery())
	fmt.Println(token, err)
}
Output:
mF_9.B5f-4.1JqM <nil>

Types

type Challenge

type Challenge struct {
	// Realm scopes the protection space (§3). Optional.
	Realm string
	// Scope is a space-delimited list of scopes that would satisfy the
	// resource (§3); typically set alongside ErrorInsufficientScope.
	Scope string
	// Error is a §3.1 error code: one of ErrorInvalidRequest,
	// ErrorInvalidToken, or ErrorInsufficientScope. Empty for a bare challenge.
	Error string
	// ErrorDescription is human-readable detail for a developer (§3.1). It
	// MUST NOT be shown to end users.
	ErrorDescription string
	// ErrorURI points to a human-readable page about the error (§3.1).
	ErrorURI string
	// Extra carries additional auth-params not modeled above, such as the
	// RFC 9728 resource_metadata parameter. Keys are rendered verbatim in
	// sorted order; values are quoted like the named fields.
	Extra map[string]string
}

Challenge is a typed WWW-Authenticate: Bearer challenge (§3). The zero value is a valid bare challenge — String reports just "Bearer" — which is what a resource server returns, with a 401, for a request that carried no credentials. Set Error to one of the Error* codes to turn it into a §3.1 error challenge; Challenge.Respond then derives the status via StatusFor.

Auth-param values are emitted as quoted-strings with " and \ escaped. Only the attributes the spec defines are rendered from the named fields; Extra carries any additional auth-params (for example RFC 9728's resource_metadata), rendered after the named ones in sorted key order. Empty fields are omitted.

func (Challenge) Respond added in v0.2.0

func (c Challenge) Respond(w http.ResponseWriter)

Respond writes a complete bodyless challenge response on w: it sets the WWW-Authenticate header (via Challenge.SetHeader) and writes the HTTP status for c.Error (via StatusFor) — a bare challenge and invalid_token yield 401, invalid_request 400, insufficient_scope 403. Like http.ResponseWriter.WriteHeader it commits the response, so call it once and before any body. When you need to write your own status or body, call Challenge.SetHeader instead and write the response yourself.

func (Challenge) SetHeader added in v0.2.0

func (c Challenge) SetHeader(w http.ResponseWriter)

SetHeader sets the WWW-Authenticate response header to c.String() on w. It writes no status and no body, so it composes with any response the caller builds — use it when you write your own status and body, for example a 401 that also carries a structured error document. For the common bodyless challenge, see Challenge.Respond.

func (Challenge) String

func (c Challenge) String() string

String renders the WWW-Authenticate header value, for example:

Bearer realm="example", error="invalid_token", error_description="The access token expired"

A zero-value Challenge renders as "Bearer".

type ExtractOption

type ExtractOption func(*extractConfig)

ExtractOption configures Token. The Authorization header (§2.1) is always read; options enable the spec's discouraged transport methods.

func WithFormBody

func WithFormBody() ExtractOption

WithFormBody enables extraction from the form-encoded request body (§2.2): an access_token parameter in an application/x-www-form-urlencoded body. Per the spec this method is never honored on GET requests, and it parses the request body (via http.Request.ParseForm) as a side effect. It is opt-in because most resource servers do not accept tokens in the body.

func WithURIQuery

func WithURIQuery() ExtractOption

WithURIQuery enables extraction from the URI query string (§2.3): an access_token query parameter. The spec marks this method SHOULD NOT — query strings leak into logs, browser history, and Referer headers — so it is off by default. A server that enables it MUST send Cache-Control: no-store on responses to such requests (§2.3); this package does not set that header for you.

Jump to

Keyboard shortcuts

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