odysseus-dev/odysseus · error · HTTPException

API token missing required scope: {required}

Error message

API token missing required scope: {required}

What it means

Raised as HTTP 403 by _scope_owner when a request authenticated with an API token lacks every one of the scopes in the route's allowed set (no intersection). API-token callers are authorized purely by scopes, unlike cookie-session callers who go through require_user. The message names the exact scopes that would have been accepted.

Source

Thrown at routes/codex_routes.py:91

        return result
    finally:
        request.state.current_user = orig
        if orig_api_token is None:
            try:
                delattr(request.state, "api_token")
            except AttributeError:
                pass
        else:
            request.state.api_token = orig_api_token


def _scope_owner(request: Request, allowed: set[str]) -> str:
    """Return the data owner if the caller is allowed for this Codex action."""
    if getattr(request.state, "api_token", False):
        scopes = set(getattr(request.state, "api_token_scopes", []) or [])
        if not scopes.intersection(allowed):
            required = " or ".join(sorted(allowed))
            raise HTTPException(403, f"API token missing required scope: {required}")
        owner = getattr(request.state, "api_token_owner", None)
        if not owner:
            raise HTTPException(403, "API token has no owner")
        return owner
    return require_user(request)


def _scope_owner_all(request: Request, required: set[str]) -> str:
    """Return owner only when an API token has every required scope."""
    if getattr(request.state, "api_token", False):
        scopes = set(getattr(request.state, "api_token_scopes", []) or [])
        missing = required - scopes
        if missing:
            raise HTTPException(403, f"API token missing required scope: {' and '.join(sorted(missing))}")
        owner = getattr(request.state, "api_token_owner", None)
        if not owner:
            raise HTTPException(403, "API token has no owner")
        return owner

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Read the message — it lists the accepted scopes; mint a new API token that includes one of them (via the API token management endpoint).
  2. Inspect the token's current scopes via the token introspection/management endpoint to confirm what it carries.
  3. If scopes were renamed in an upgrade, re-issue the token with the new scope names.
  4. Alternatively call the route with a cookie session instead of the API token.

Example fix

# before
curl -H 'Authorization: Bearer $TOKEN' https://host/api/codex/emails  # 403

# after
TOKEN=$(create_token --scope email:read)
curl -H "Authorization: Bearer $TOKEN" https://host/api/codex/emails
Defensive patterns

Strategy: validation

Validate before calling

const resp = await fetch('/api/tokens/introspect', {headers: auth});
const {scopes} = await resp.json();
const needed = ['email:read']; // from route docs
if (!needed.some(s => scopes.includes(s))) {
  throw new Error(`Token lacks scope; has [${scopes}] needs one of [${needed}]`);
}

Type guard

function tokenAllows(tokenScopes: string[], required: string[]): boolean {
  return required.some(s => tokenScopes.includes(s));
}

Try / catch

try { r = await codexListEmails() } catch (e) { if (e.status === 403 && e.detail.startsWith('API token missing required scope')) { await mintTokenWithScopes(parseScopes(e.detail)); retry once } else throw }

Prevention

When it happens

Trigger: Calling a /api/codex/* endpoint (e.g. GET /api/codex/emails) with a token whose scope list does not include any of EMAIL_READ_SCOPES (or the route's specific set); using a read-only token against a write route; typo in a custom token's scope string.

Common situations: Token minted with a minimal scope set for a different integration; scopes renamed between app versions so old tokens no longer match; using a token created for the cookbook on email routes.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/fb07679c1269ccf1. Report an issue: GitHub.