Commit e337a6f
Add OAuth 2.1 resource-server package (#70)
* Add OAuth 2.1 resource-server package
pkg/oauth implements the resource-server side of OAuth for the MCP
server, validated against the authorization server by RFC 7662 token
introspection (the MCP server has no direct access to token storage):
- Config loaded from OAUTH_* env vars, validated at startup, with the
resource URI held in RFC 3986 canonical form so audience comparison
is immune to equivalent spellings.
- RFC 9728 protected-resource metadata document, served at both the
path-insertion well-known URI (RFC 9728 section 3.1) and the root
form.
- RFC 6750 bearer middleware: introspected OAuth tokens are checked
against this server's audience; non-OAuth bearers pass through to the
Render API unchanged so existing API-key users are unaffected.
Introspection outages fail closed as 503, not invalid_token, so
clients don't discard valid tokens during a blip.
- Introspection client with a bounded in-process cache (sha256 keys,
TTL capped by token expiry, negative caching) and collapsing of
concurrent lookups for the same token.
Nothing is wired into the server yet; that lands separately behind
OAUTH_ENABLED.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Reject query and fragment components in OAuth config URLs
A query on either URL would corrupt derived URLs built by concatenation
(the introspection endpoint, the RFC 9728 path-insertion metadata URL —
which previously dropped the query while audience matching preserved
it), and RFC 8707 forbids fragments in resource indicators. Neither
component identifies anything for these values, so fail at startup
instead of carrying them inconsistently.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Accept bare API keys through the OAuth middleware and harden config edges
Round-2 self-review fixes:
- Clients may send API keys without the Bearer scheme — the existing
header parsing accepts that, so enabling OAuth must not 401 them.
Credential extraction moves to a shared authn.BearerToken helper
(strip the scheme case-insensitively when present, else use the raw
value) so the middleware and the server's context func can never
disagree about what the credential is. Bare credentials flow through
the same introspection path: bare API keys pass through exactly as
before, and a bare OAuth token still gets the full audience check.
- Boolean env vars now reject unrecognized values instead of silently
treating them as false: OAUTH_ENABLED=on previously disabled OAuth
and skipped all startup validation; a typo'd OAUTH_API_KEY_PASSTHROUGH
silently enabled strict mode.
- The query/fragment guard checks the raw string, catching a bare
trailing "?" or "#" (url.Parse reports those as no query/fragment)
that would corrupt the concatenated introspection URL.
- Audience values are canonicalized once at decode, so the per-request
audience check is plain string equality; the middleware canonicalizes
its configured resource at construction so hand-built Configs can't
silently break matching, and precomputes the challenge string.
- Client disconnects while waiting on introspection are no longer
logged as introspection failures (they'd pollute the outage signal).
- The introspection 401 error distinguishes "no service token
configured" from "service token rejected".
- Drop the unused IssuedAt field; consolidate duplicate test fixtures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Use BearerToken in ContextWithAPITokenFromHeader
The helper's contract is that every Authorization parser goes through
it; adopt it at the one existing parse site in the same PR that
introduces it. Behavior change is limited to case-variant schemes
("bearer x"), which RFC 7235 says must be accepted and which previously
failed downstream with the prefix left in place.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address PR review: split cache file, nearest-expiry eviction, table tests
- Move tokenCache into its own file (tokencache.go) so introspect.go is
just the introspection client; tests move alongside it.
- Eviction now drops the entries closest to expiry (not arbitrary
map-iteration order) down to three-quarters capacity, so it sheds the
entries with the least useful life left and the policy is described
accurately.
- Collapse the formulaic FromEnv validation-error subtests into a
table (TestFromEnv_Errors); behavior coverage is unchanged.
- Note in wellknown.go that the metadata body is marshaled once at
startup, not per request.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address review: table-driven middleware/metadata tests, trim comments
Per review feedback (wendorf):
- Collapse the per-case TestMiddleware_* functions into one
input→output table (the structurally different disabled-identity
case stays separate); same for the TestHandleProtectedResourceMetadata
status matrix, with the JSON document-shape assertions kept as a
focused test.
- Tighten the wordier doc comments (Introspector, Introspect,
canonicalResourceURI, evictLocked, APIKeyPassthrough) without
dropping the rationale.
- Drop the self-evident fakeAS doc comment.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Use Lock/defer Unlock consistently in test helpers
Adopt the Lock() / defer Unlock() idiom in the two test lock sites that
released manually before a trailing non-critical call. The early unlock
saved nothing here, and the deferred form is harder to get wrong and
consistent with the rest of the package.
The singleflight in Introspect keeps its manual unlock on purpose: it
releases the lock before the introspection network call, so concurrent
lookups don't serialize behind one request.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
GitOrigin-RevId: e1e811c99d6e311cbdefa824267beceb7fc6b2531 parent 36f10f9 commit e337a6f
12 files changed
Lines changed: 1633 additions & 7 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
25 | 26 | | |
26 | 27 | | |
27 | 28 | | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
28 | 41 | | |
29 | 42 | | |
30 | 43 | | |
| |||
33 | 46 | | |
34 | 47 | | |
35 | 48 | | |
36 | | - | |
37 | | - | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
| 49 | + | |
43 | 50 | | |
44 | 51 | | |
45 | 52 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 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 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
0 commit comments