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"
doneObserved 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.pyexposesbare_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 aBearerprefix to it fails withInvalid header paddingand 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()instudio/ryax/studio/infrastructure/api/dependencies.py:32. - Authorization —
core/ryax/authorization/infrastructure/api/dependencies.py:35passesrequest.headers.get("Authorization", "")straight intocheck_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. ryaxctlsendsBearer— that is the reasonbare_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.