Package rfc6750

import "funnyhaha.technology/rfc/rfc6750"
View the source code

Overview

Package rfc6750 implements the parts of RFC 6750 (OAuth 2.0 Bearer Token Usage) that a resource server needs: reading a bearer token from the Authorization header field, and challenging a client that didn't send a valid one.

Build the Challenge once, while wiring the server:

bearer := rfc6750.MustChallenge("api")

Then answer each request with the status that matches the failure. The error responses set the status code that Section 3.1 gives each error code, so the two can't disagree:

rfc9110.Unauthorized(bearer.Challenge())               // No token was sent.
rfc6750.InvalidToken(bearer, "The access token expired") // 401
rfc6750.InsufficientScope(bearer, "", "users:write")     // 403
rfc6750.InvalidRequest(bearer, "")                       // 400

The package doesn't cover sending a token in a form-encoded body or a URI query parameter (Sections 2.2 and 2.3), or the error_uri attribute.

The RFC text is in rfc6750.txt in this directory, and the doc comments cite its sections.

Example

Example shows a resource server choosing the RFC 6750 response for a request with no token, an invalid token, and a valid token without the scope it needs.

package main

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

	"funnyhaha.technology/rfc/rfc6750"
	"funnyhaha.technology/rfc/rfc9110"
)

func main() {
	bearer := rfc6750.MustChallenge("api")

	authorize := func(r *http.Request) rfc9110.Status {
		token, err := rfc6750.ParseAuthorization(r.Header.Get("Authorization"))
		switch {
		case errors.Is(err, rfc6750.ErrNoToken):
			return rfc9110.Unauthorized(bearer.Challenge())
		case err != nil || token.Reveal() != "valid":
			return rfc6750.InvalidToken(bearer, "The access token is invalid")
		default:
			return rfc6750.InsufficientScope(bearer, "", "users:write")
		}
	}

	for _, header := range []string{"", "Bearer expired", "Bearer valid"} {
		r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/users", nil)
		r.Header.Set("Authorization", header)
		s := authorize(r)
		fmt.Println(s.Code(), s.Header().Get("WWW-Authenticate"))
	}
}

Output:

401 Bearer realm="api"
401 Bearer realm="api", error="invalid_token", error_description="The access token is invalid"
403 Bearer realm="api", scope="users:write", error="insufficient_scope"

Index

Variables

var (
	// ErrInvalidRealm is returned for a realm that's empty or contains a
	// control character.
	ErrInvalidRealm = errors.New("invalid realm")

	// ErrInvalidScope is returned for a scope value with characters outside
	// the set Section 3 allows.
	ErrInvalidScope = errors.New("invalid scope")

	// ErrInvalidDescription is returned for an error_description with
	// characters outside the set Section 3 allows.
	ErrInvalidDescription = errors.New("invalid error description")

	// ErrNoToken is returned by [ParseAuthorization] when the request has
	// no bearer credentials: the field is empty or uses another scheme.
	// Section 3.1 says to respond with a challenge but no error code.
	ErrNoToken = errors.New("no bearer token")

	// ErrInvalidToken is returned by [ParseAuthorization] for bearer
	// credentials that aren't a b64token. Respond with [InvalidToken].
	ErrInvalidToken = errors.New("invalid bearer token")
)
source

Functions

InsufficientScope source

func InsufficientScope(c Challenge, d Description, need Scope, more ...Scope) rfc9110.Status

InsufficientScope returns 403 Forbidden with an insufficient_scope challenge listing the scopes the request needs, in place of any set with Challenge.WithScope (Section 3.1).

InvalidRequest source

func InvalidRequest(c Challenge, d Description) rfc9110.Status

InvalidRequest returns 400 Bad Request with an invalid_request challenge: the request is missing a parameter, repeats one, uses more than one way of sending a token, or is otherwise malformed (Section 3.1).

InvalidToken source

func InvalidToken(c Challenge, d Description) rfc9110.Status

InvalidToken returns 401 Unauthorized with an invalid_token challenge: the token is expired, revoked, malformed or otherwise invalid (Section 3.1).

Types

Challenge source

type Challenge struct {
	// contains filtered or unexported fields
}

Challenge is a Bearer challenge for a realm, with optional scopes. Build it once, with NewChallenge or MustChallenge.

See RFC 6750, Section 3.

MustChallenge source

func MustChallenge(realm string) Challenge

MustChallenge is like NewChallenge but panics if realm isn't valid. Use it while wiring a server, so a bad realm stops it starting.

NewChallenge source

func NewChallenge(realm string) (Challenge, error)

NewChallenge returns a challenge for the realm. The realm is required, because Section 3 says the scheme "MUST be followed by one or more auth-param values". It returns ErrInvalidRealm if realm is empty or contains a control character.

(Challenge) Challenge source

func (c Challenge) Challenge() rfc9110.Challenge

Challenge returns c as an rfc9110.Challenge, with no error code. Send it when a request has no token (Section 3.1):

rfc9110.Unauthorized(bearer.Challenge())

(Challenge) WithParam source

func (c Challenge) WithParam(p rfc9110.AuthParam) Challenge

WithParam returns a copy of c whose challenges carry an extension's auth-param, such as RFC 9728's resource_metadata. Section 3 allows them: "Additional authentication parameters MAY be used". A parameter with the name of one this package sets (realm, scope, error or error_description), or of one already added, is left out of the challenges.

(Challenge) WithScope source

func (c Challenge) WithScope(s Scope, more ...Scope) Challenge

WithScope returns a copy of c whose challenges list the scopes a token needs to access the resource.

Description source

type Description string

Description is a human-readable explanation of an error, for developers rather than end users. The empty Description is left out. Use ParseDescription to check a string.

See RFC 6750, Section 3.

ParseDescription source

func ParseDescription(s string) (Description, error)

ParseDescription returns s as a Description, or ErrInvalidDescription if it has a character outside %x20-21 / %x23-5B / %x5D-7E.

Scope source

type Scope string

Scope is one scope value, such as "users:write". Use ParseScope to check a string.

See RFC 6750, Section 3.

ParseScope source

func ParseScope(s string) (Scope, error)

ParseScope returns s as a Scope, or ErrInvalidScope if it's empty or has a character outside %x21 / %x23-5B / %x5D-7E. A Scope is a single value, so it can't contain a space.

Token source

type Token struct {
	// contains filtered or unexported fields
}

Token is a bearer access token. Its String method doesn't reveal the token, so logging a Token by accident doesn't leak it. Use Token.Reveal to get the value.

ParseAuthorization source

func ParseAuthorization(value string) (Token, error)

ParseAuthorization returns the bearer token in an Authorization field value, which has the form "Bearer" 1*SP b64token (Section 2.1). The scheme is matched case-insensitively. It returns ErrNoToken if the value is empty or uses another scheme, and ErrInvalidToken if the credentials aren't a b64token.

(Token) GoString source

func (t Token) GoString() string

GoString returns a placeholder, not the token, so %#v doesn't reveal it either.

(Token) Reveal source

func (t Token) Reveal() string

Reveal returns the token's value.

(Token) String source

func (t Token) String() string

String returns a placeholder, not the token.