Authorization header: Bearer prefix rejected by the Authorization and Repository services

Authorization: Bearer <jwt> works on the Runner and the Studio and is rejected with 401 by the Authorization and Repository services. Our own README tells users to send Bearer, so the documented recipe fails on two of the four services.

Steps to reproduce

Against any instance, with a JWT from POST /api/authorization/login:

for p in /api/authorization/v2/projects /api/repository/v2/sources \
         /api/studio/workflows /api/runner/sites; do
  RAW=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost$p" -H "Authorization: $JWT")
  BEA=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost$p" -H "Authorization: Bearer $JWT")
  printf "%-46s raw=%s bearer=%s\n" "$p" "$RAW" "$BEA"
done

Observed on 26.9.0:

Endpoint Authorization: <jwt> Authorization: Bearer <jwt>
/api/authorization/v2/projects 200 401
/api/repository/v2/sources 200 401
/api/studio/workflows 200 200
/api/runner/sites 200 200

The body is {"error":"Access denied"}.

Why

Four services, three behaviours, and the prefix-stripping is implemented twice:

  • Runner — core/ryax/runner/infrastructure/api/authorization_header.py exposes bare_token(), which strips the prefix. Its docstring already describes this exact failure: "the authorization service decodes the whole header value it is given, so forwarding a Bearer prefix to it fails with Invalid header padding and turns every project-scoped request into a 401", and asks that it stay "the single place that knows about the prefix". It is currently used only inside the Runner.
  • Studio — a second, private implementation, _get_token() in studio/ryax/studio/infrastructure/api/dependencies.py:32.
  • Authorization — core/ryax/authorization/infrastructure/api/dependencies.py:35 passes request.headers.get("Authorization", "") straight into check_access(), prefix and all.
  • Repository — same shape, repository/ryax/repository/infrastructure/api/dependencies.py:40.

All four declare HTTPBearer(auto_error=False) for the OpenAPI lock icon, so the generated API reference shows a bearer scheme on every endpoint — including the two where sending a bearer token does not work.

Impact

  • The README's headless install recipe (README.md, the "Create the site and node pool from the API instead" block) uses -H "Authorization: Bearer $JWT". It happens to work there because those calls are Runner endpoints, but anyone copying the pattern to an authorization or repository call gets an opaque 401.
  • ryaxctl sends Bearer — that is the reason bare_token() exists — so any of its commands hitting Authorization or Repository endpoints would fail the same way.
  • The published API reference advertises bearer auth for all of it.

Possible fix

Move bare_token() somewhere shared and use it in all four services' get_current_user_id dependencies, then drop Studio's _get_token(). Note ryax.common currently lives only in the runner submodule, so "shared" needs a decision rather than just an import.

Minor, while in there: Studio's _get_token() does token.split(" ")[1] after a startswith("Bearer") test, so a header of exactly Bearer raises IndexError and returns a 500 rather than a 401.

Found while verifying old issues against a local install.