Package rfc9068

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

Overview

Package rfc9068 implements the JWT Profile for OAuth 2.0 Access Tokens, from RFC 9068: the claims an access token carries, and the checks a resource server makes before it accepts one.

It doesn't verify signatures, which needs a JOSE library. A resource server verifies the JWS with the authorization server's keys, checks its typ header with CheckType, unmarshals the payload into Claims, and calls Claims.Validate:

err := rfc9068.CheckType(typ)
var claims rfc9068.Claims
err = json.Unmarshal(payload, &claims)
err = claims.Validate(rfc9068.Expected{
	Issuer:   "https://as.example.com",
	Audience: rfc8707.MustResource("https://mcp.example.com/mcp"),
	Now:      time.Now(),
	Leeway:   30 * time.Second,
})

Section 4 says every failed check is answered with RFC 6750's invalid_token error.

The package doesn't cover encrypted tokens (Section 4) or the authorization claims other than scope (Section 2.2.3), which go through Claims.Extension.

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

Index

Constants

const (
	ClaimIssuer   = "iss"
	ClaimExpiry   = "exp"
	ClaimAudience = "aud"
	ClaimSubject  = "sub"
	ClaimClientID = "client_id"
	ClaimIssuedAt = "iat"
	ClaimID       = "jti"
)
source

The names of the claims Section 2.2 requires, for ErrMissingClaim.

const MediaType = "application/at+jwt"
source

MediaType is the media type of a JWT access token (Section 7.2).

const TokenType = "at+jwt"
source

TokenType is the typ header of a JWT access token. MediaType is its full form, which Section 4 accepts too.

See RFC 9068, Section 2.1.

Variables

var (
	// ErrWrongType is returned by [CheckType] for a typ header that isn't
	// at+jwt.
	ErrWrongType = errors.New("not a JWT access token")

	// ErrInvalidClaims is returned by [Claims.UnmarshalJSON] for a claim
	// with the wrong JSON type, such as a string exp.
	ErrInvalidClaims = errors.New("invalid claims")

	// ErrMissingClaim is returned by [Claims.Validate] for a token without
	// a claim Section 2.2 requires.
	ErrMissingClaim = errors.New("missing claim")

	// ErrWrongIssuer is returned by [Claims.Validate] when iss isn't the
	// expected issuer.
	ErrWrongIssuer = errors.New("wrong issuer")

	// ErrWrongAudience is returned by [Claims.Validate] when aud doesn't
	// contain the expected resource.
	ErrWrongAudience = errors.New("wrong audience")

	// ErrExpired is returned by [Claims.Validate] for a token past its
	// exp.
	ErrExpired = errors.New("token expired")

	// ErrNotYetValid is returned by [Claims.Validate] for a token whose
	// iat or nbf is in the future.
	ErrNotYetValid = errors.New("token not yet valid")
)
source

Functions

CheckType source

func CheckType(typ string) error

CheckType returns ErrWrongType unless typ, a JWT's typ header, is at+jwt or application/at+jwt. Section 4 says a resource server MUST reject any other value. Media type names are case-insensitive (RFC 7515, Section 4.1.9).

Types

Claims source

type Claims struct {
	Issuer    string
	Subject   string
	Audience  []string
	Expiry    time.Time
	NotBefore time.Time
	IssuedAt  time.Time
	ID        string
	ClientID  string
	Scope     oauth21draft.Scope
	// contains filtered or unexported fields
}

Claims are an access token's claims: those Section 2.2 requires, scope from Section 2.2.3, the optional nbf, and any others as extension members. Set the standard claims directly, and others with Claims.WithExtension.

Claims has its own MarshalJSON and UnmarshalJSON. Times are NumericDate seconds, and aud is marshalled as a string when it has one value, as RFC 7519, Section 4.1.3, allows.

(Claims) Extension source

func (c Claims) Extension(name string, v any) (bool, error)

Extension unmarshals the extension claim with the name into v, as json.Unmarshal does. It reports false if c has no such claim.

(Claims) MarshalJSON source

func (c Claims) MarshalJSON() ([]byte, error)

MarshalJSON returns c as a JWT claims set: the standard claims, leaving out empty ones, then extension claims sorted by name.

(Claims) UnmarshalJSON source

func (c *Claims) UnmarshalJSON(data []byte) error

UnmarshalJSON reads a JWT claims set, keeping claims it doesn't define as extension claims. It returns ErrInvalidClaims for a claim of the wrong type, a time that isn't a finite number, or a scope that isn't valid.

(Claims) Validate source

func (c Claims) Validate(want Expected) error

Validate makes the checks Section 4 lists that don't need the token's signature or header: that the claims Section 2.2 requires are present (ErrMissingClaim), that iss matches exactly (ErrWrongIssuer), that aud contains the resource (ErrWrongAudience), and that Now is before exp (ErrExpired). It also refuses a token whose iat or nbf is after Now (ErrNotYetValid), as RFC 7519, Section 4.1.5, requires for nbf. Each time is allowed the Leeway.

Resources are compared as rfc8707.Resource.Equal does, so the scheme and host are case-insensitive.

(Claims) WithExtension source

func (c Claims) WithExtension(name string, value any) Claims

WithExtension returns a copy of c with an extension claim, replacing any with the same name. The value is marshalled when c is, so Claims.MarshalJSON returns an error if it can't be, or if the name is a standard claim's.

Expected source

type Expected struct {
	// Issuer is the authorization server's issuer identifier, which iss
	// must match exactly.
	Issuer string

	// Audience is the resource server's own resource indicator, which aud
	// must contain.
	Audience rfc8707.Resource

	// Now is the time to check exp, nbf and iat against.
	Now time.Time

	// Leeway allows for clock skew between the servers. Section 4 says it's
	// "usually no more than a few minutes".
	Leeway time.Duration
}

Expected is what a resource server expects of a token.