Commit 97ee0af
authored
fix(auth): match the bearer scheme case-insensitively (#198)
Closes #188
Rebased onto `main` after #197 landed. That PR touched the same
functions, so this one is now scoped down to the single thing #188 is
about — see "Relationship to #197" below.
## Summary
- HTTP authentication schemes are case-insensitive (RFC 9110 §11.1), but
`readBearerCredential` matched the header against a literal `Bearer `
prefix. Clients that normalize the scheme to `bearer` or `BEARER` were
rejected with 401 even with a valid token.
- Split the header into scheme + credentials and compare the scheme with
`toLowerCase()`. Nothing else about the parse changes.
## Relationship to #197
#188 asked for two things: case-insensitive scheme matching, and a
constant-time compare for the secret. **#197 already landed the
constant-time half** (`matchesConfiguredToken` / `constantTimeEqual`).
This PR is only the scheme half, layered on top — 8 lines of production
change.
#197 also made a deliberate decision I have preserved rather than
reworked: `readBearerCredential` returns the credential **verbatim**, so
configured tokens still require a byte-for-byte match. Concretely, that
means:
- Repeated separator spaces are still rejected. RFC 9110's ABNF is
`1*SP`, so `bearer token` is arguably legal, but #197 pins `Bearer
admin-secret` → 401, and loosening it would silently reverse that call
in a PR about scheme casing. Left alone; worth a separate issue if
anyone wants it.
- No `.trim()` was added anywhere. The credential still reaches
`constantTimeEqual` exactly as sent.
The only behavior that changes is which scheme spellings parse.
## Changes
`readBearerCredential` now finds the first space, compares `slice(0,
separator).toLowerCase()` against `"bearer"`, and returns the remainder
untouched. It is the only place in `src/` that reads an inbound
`Authorization` header, so both call sites (`matchesConfiguredToken` for
the env tokens, `readBearerToken` for runtime-token lookup) pick the fix
up.
Deliberately **not** widened:
- `Bearer\t<token>` still 401s — RFC 9110 allows only `1*SP` as the
separator, and `\s`-based splits over-accept HTAB/NBSP, creating a
parser differential with strict intermediaries.
- `BearerX ...` / `Bearer-Foo ...` still 401 — the scheme is compared as
a whole slice, not with `startsWith`.
`toLowerCase().startsWith("bearer")` would accept these and hand back `X
<token>` as the credential.
## Test plan
- [x] `npm test -- src/server/api/auth.test.ts` — scheme-casing matrix
across all three auth scopes (`/api`, `/v1`, `/mcp`), cookie issuance
for a lowercase scheme, admin elevation on `POST /v1/actions` with
`BEARER`, and the dynamic runtime-token resolver path with a lowercase
scheme (asserting it receives `"oct_valid"`, not a padded string).
- [x] `npm test -- src/server/connect-server.test.ts` —
`/api/auth/session` is a public path that authenticates through
`readLocalAuthSession` rather than the middleware, so it needs its own
coverage; plus an end-to-end `bearer oct_…` runtime-token call and a
`bearer local-token` → 401 on `/v1/actions` proving the scope boundary
did not move with the scheme.
- [x] 6-case negative matrix, chosen not to overlap #197's byte-for-byte
test. The load-bearing ones are `bearer admin-Secret` (credentials stay
case-sensitive even when the scheme is lowercased — this catches the
likeliest wrong fix, lowercasing the whole header) and `bearer
admin-secret` (pins that case-insensitivity did not loosen separator
handling either).
- [x] Verified the new assertions are load-bearing: 8 of them fail
against `origin/main` as it stands today, post-#197.
- [x] `npm run fix-check` and the full `npm test` (548 tests) pass.
- [x] Differential check old-vs-new over a large header/config matrix:
zero inputs that authenticated before and 401 now. Every difference is
the intended widening.
## Notes
Checked on both runtimes, since this file runs on Node and on Workers
via `cloudflare.ts`. The change adds no imports and nothing outside
plain ECMAScript.
Two adjacent gaps found while working on this, left out as out of scope:
- 401 responses carry no `WWW-Authenticate` header anywhere in the repo
(RFC 9110 §15.5.2 requires it; RFC 6750 §3 gives the Bearer form).
MCP/OAuth clients use it to decide whether to refresh.
- `src/server/api/openapi.ts` emits no `securitySchemes`, though
`docs/runtime-api.md` advertises `/openapi.json` for importers.
Happy to file either separately.1 parent 6f172d3 commit 97ee0af
3 files changed
Lines changed: 113 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| |||
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
72 | 127 | | |
73 | 128 | | |
74 | 129 | | |
| |||
102 | 157 | | |
103 | 158 | | |
104 | 159 | | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
105 | 169 | | |
106 | 170 | | |
107 | 171 | | |
| |||
130 | 194 | | |
131 | 195 | | |
132 | 196 | | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
266 | 267 | | |
267 | 268 | | |
268 | 269 | | |
269 | | - | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
270 | 276 | | |
271 | 277 | | |
272 | | - | |
273 | | - | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
274 | 284 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1102 | 1102 | | |
1103 | 1103 | | |
1104 | 1104 | | |
| 1105 | + | |
| 1106 | + | |
| 1107 | + | |
| 1108 | + | |
| 1109 | + | |
| 1110 | + | |
| 1111 | + | |
| 1112 | + | |
| 1113 | + | |
| 1114 | + | |
| 1115 | + | |
1105 | 1116 | | |
1106 | 1117 | | |
1107 | 1118 | | |
| |||
1183 | 1194 | | |
1184 | 1195 | | |
1185 | 1196 | | |
| 1197 | + | |
| 1198 | + | |
| 1199 | + | |
| 1200 | + | |
| 1201 | + | |
| 1202 | + | |
| 1203 | + | |
| 1204 | + | |
| 1205 | + | |
| 1206 | + | |
| 1207 | + | |
1186 | 1208 | | |
1187 | 1209 | | |
1188 | 1210 | | |
| |||
0 commit comments