Who may do what

The cart’s bearer tokens: the claims it reads, verifying JWTs against the identity provider’s key set, a token with no subject, a key set that is down, the carts:write scope, and keeping each cart to its owner.

Every operation needs a JWT bearer token from the identity provider. Changing a cart needs the scope carts:write too, and a cart is only ever its owner’s.

The claims

The claims the cart reads from a token are a type of its own. HasScope is what auth.RequireScopes asks:

const ScopeWrite = "carts:write"

type Claims struct {
	Subject string `json:"sub"`
	Scope   string `json:"scope"`
}

func (c Claims) HasScope(scope string) bool {
	return slices.Contains(strings.Fields(c.Scope), scope)
}

Verifying tokens

Init builds an httpclient client for the key set’s origin, a jwt.RemoteKeySet that fetches the key set with it, and a Verifier of Claims:

idp, err := httpclient.NewClient(s, settings.KeySet.Client)
if err != nil {
	return nil, err
}
keys, err := jwt.NewRemoteKeySet(settings.KeySet.URL, idp)
if err != nil {
	return nil, err
}
tokens, err := jwt.NewVerifier[endpoint.Claims](s, settings.JWT, keys)
if err != nil {
	return nil, err
}

The client’s base URL is the key set URL’s origin:

read.KeySet.Client.BaseURL = &url.URL{Scheme: read.KeySet.URL.Scheme, Host: read.KeySet.URL.Host}

The Verifier checks the signature, the issuer, the audience, the algorithm and the times, as JWT_* sets them.

A token with no subject

A cart is its owner’s, so a token with no sub is a 401:

func withSubject(verify func(context.Context, string) (endpoint.Claims, error)) func(context.Context, string) (endpoint.Claims, error) {
	return func(ctx context.Context, token string) (endpoint.Claims, error) {
		claims, err := verify(ctx, token)
		if err != nil {
			return endpoint.Claims{}, err
		}
		if claims.Subject == "" {
			return endpoint.Claims{}, fmt.Errorf("the token has no sub: %w", auth.ErrUnauthenticated)
		}
		return claims, nil
	}
}

A key set that is down

A key set that can’t be fetched, before any key was, is a *jwt.KeySetError. The token may be good, so it is a 503 Unavailable, not a 401:

func KeysUnavailable(verify func(context.Context, string) (Claims, error)) func(context.Context, string) (Claims, error) {
	return func(ctx context.Context, token string) (Claims, error) {
		claims, err := verify(ctx, token)
		if _, ok := errors.AsType[*jwt.KeySetError](err); ok {
			return Claims{}, unavailable("The server can't verify tokens right now. Try again later.")
		}
		return claims, err
	}
}

The bearer scheme

The scheme wraps the verify function in both:

bearer := auth.Bearer("bearer", endpoint.KeysUnavailable(withSubject(tokens.Verify))).
	Format("JWT").
	Description("A JWT from the shoppingcart's identity provider. Changing a cart needs the scope carts:write.")

Requiring it

The API requires the scheme on every operation:

openapi.Security(auth.Require(bearer)),

Every operation that changes a cart requires carts:write too:

write := openapi.Security(auth.RequireScopes(c.Bearer, ScopeWrite))

The document is open to anyone:

openapi.Document(openapi.Root().Lit("openapi.json"), openapi.Security(auth.Disabled())),

A cart’s owner

Each handler takes the owner from the claims the scheme verified:

func (c Carts) owner(ctx context.Context) (string, error) {
	claims, ok := c.Bearer.From(ctx)
	if !ok || claims.Subject == "" {
		return "", errors.New("the request has no bearer token with a subject")
	}
	return claims.Subject, nil
}

A cart is created with its owner:

err := s.pool.QueryRow(ctx,
	`INSERT INTO carts (owner) VALUES ($1) RETURNING id, created_at, updated_at`, owner,
).Scan(&c.CartID, &c.CreatedAt, &c.UpdatedAt)

And every query after that matches the owner as well as the ID:

tag, err := s.pool.Exec(ctx, `DELETE FROM carts WHERE id = $1 AND owner = $2`, cartID, owner)

So another subject’s cart is ErrCartNotFound, a 404, and the API never says whose carts exist.

The answers

The requestThe answer
no token, or one that doesn’t verify401
a token with no sub401
a change without carts:write403
another subject’s cart404
a key set that can’t be fetched503

Authentication and authorization describes each piece.