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:
- extract the token from the request with Token;
- validate it (signature, claims, scope) with the appropriate library;
- 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 ¶
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.
const SpecVersion = "RFC 6750"
SpecVersion is the version of the specification this package targets.
Variables ¶
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 ¶
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.
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.